diff --git a/.gitignore b/.gitignore index 0473c9ce3..6a104ee97 100644 --- a/.gitignore +++ b/.gitignore @@ -34,3 +34,7 @@ gradle.properties # Local debug settings app/src/debug/raw/survey.properties + +#Builds +builds/ +tmp/ diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 778e93499..a2abb9bfb 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,30 @@ Akvo FLOW app release notes =========================== +# ver 2.2.10 +Date: 19 January 2017 + +# New and noteworthy +* **Update play services** - [#508] (https://github.com/akvo/akvo-flow-mobile/issues/508) Using Google Play Services version 7.5.0 with new features and bug fixes. +* **Add basic lint configuration** - [#532] (https://github.com/akvo/akvo-flow-mobile/issues/532) Lint can now be run before building the app. +* **Move Version code and name to properties file** - [#555] (https://github.com/akvo/akvo-flow-mobile/issues/555) It is now easier to increase version name and code. +* **Remove location beacon sending** - [#550] (https://github.com/akvo/akvo-flow-mobile/issues/550) The location beacon sending feature, disabled by default, has now been completely removed. +* **Design changes after update of support library** - [#559] (https://github.com/akvo/akvo-flow-mobile/issues/559) The Android Support Library has been updated to version 25.0.1 with multiple cosmetic improvements and fixes. +* **Replace Akvo FLOW by Akvo Flow string** - [#564] (https://github.com/akvo/akvo-flow-mobile/issues/564) The app naming is now consistent everywhere in the app. +* **Pull latest translations from Transifex** - [#589] (https://github.com/akvo/akvo-flow-mobile/issues/589) The translations have been updated. + +# Resolved issues +* **Change the query type for the data point search** - [#467] (https://github.com/akvo/akvo-flow-mobile/issues/467) You can now easily search for a data point using any of the name fields, not just the first word. +* **Error notification icon is shown as empty white circle** - [#486] (https://github.com/akvo/akvo-flow-mobile/issues/486) Notification look has been improved with new icon and colors. +* **When notifications have long text, only one line is shown** - [#519] (https://github.com/akvo/akvo-flow-mobile/issues/519) Notifications can now be expanded on newer devices and the text has been made clearer and shorter. +* **Error notification too long for unsuccessful syncing of data points** - [#560] (https://github.com/akvo/akvo-flow-mobile/issues/560) Notifications for data point syncing errors now have shorter text (similar to #519). +* **Syncing imported data points** - [#526] (https://github.com/akvo/akvo-flow-mobile/issues/526) You will be notified if the data points were not synced correctly. +* **When device is rotated user is shown the download update dialog again** - [#499] (https://github.com/akvo/akvo-flow-mobile/issues/499) You will no longer be constantly shown the update dialog when rotating the device. +* **How to notify user of available updates** - [#578] (https://github.com/akvo/akvo-flow-mobile/issues/578) Related to #499, the update frequency and user notification of new updates has been reduced. +* **Form name does not wrap** - [#521] (https://github.com/akvo/akvo-flow-mobile/issues/521) Long form names are now displayed correctly. +* **"About Akvo" is outdated** - [#545] (https://github.com/akvo/akvo-flow-mobile/issues/545) The "About Akvo" screen now shows up to date information. +* **Long cascade options do not wrap** - [#568] (https://github.com/akvo/akvo-flow-mobile/issues/568) Long cascade names are now visible in full. + +--------------- # ver 2.2.9 Date: 24 November 2016 diff --git a/app/build.gradle b/app/build.gradle index ff25990f0..2cbb74782 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,68 +1,89 @@ apply plugin: 'com.android.application' -buildscript { - repositories { - mavenCentral() - } - dependencies { - classpath 'com.android.tools.build:gradle:2.1.0' - } -} - repositories { mavenCentral() } android { - compileSdkVersion 21 - buildToolsVersion "21.1.2" - defaultConfig { - minSdkVersion 10 - targetSdkVersion 21 - applicationId "org.akvo.flow" - testApplicationId "org.akvo.flow.tests" - testInstrumentationRunner "android.test.InstrumentationTestRunner" - testHandleProfiling true - testFunctionalTest true - } - compileOptions { - sourceCompatibility JavaVersion.VERSION_1_7 - targetCompatibility JavaVersion.VERSION_1_7 - } - signingConfigs { + compileSdkVersion 25 + buildToolsVersion "25.0.1" + def versionPropsFile = file('version.properties') - debug { - keyAlias 'androiddebugkey' - keyPassword 'android' - storeFile file('debug-key/debug.keystore') - storePassword 'android' - } + if (versionPropsFile.canRead()) { + def Properties versionProps = new Properties() + + versionProps.load(new FileInputStream(versionPropsFile)) + + def versionMajor = versionProps['VERSION_MAJOR'].toInteger() + def versionMinor = versionProps['VERSION_MINOR'].toInteger() + def versionPatch = versionProps['VERSION_PATCH'].toInteger() + def versionCodeProperty = versionProps['VERSION_CODE'].toInteger() - flowRelease { - storeFile file(RELEASE_STORE_FILE) - storePassword RELEASE_STORE_PASSWORD - keyAlias RELEASE_KEY_ALIAS - keyPassword RELEASE_KEY_PASSWORD + defaultConfig { + versionCode versionCodeProperty + versionName "${versionMajor}.${versionMinor}.${versionPatch}" + minSdkVersion 10 + targetSdkVersion 21 + applicationId "org.akvo.flow" + testApplicationId "org.akvo.flow.tests" + testInstrumentationRunner "android.test.InstrumentationTestRunner" + testHandleProfiling true + testFunctionalTest true } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_7 + targetCompatibility JavaVersion.VERSION_1_7 + } + signingConfigs { + + debug { + keyAlias 'androiddebugkey' + keyPassword 'android' + storeFile file('debug-key/debug.keystore') + storePassword 'android' + } - buildTypes { - release { - signingConfig signingConfigs.flowRelease + flowRelease { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + } + buildTypes { + release { + signingConfig signingConfigs.flowRelease + } } } - } - packagingOptions { - exclude 'META-INF/LICENSE' - exclude 'META-INF/NOTICE' - } + packagingOptions { + exclude 'META-INF/LICENSE' + exclude 'META-INF/NOTICE' + } - productFlavors { - flow { + productFlavors { + flow { + } + biogas { + } + cookstoves { + } } - biogas { + + testOptions { + unitTests.returnDefaultValues = true } - cookstoves { + + lintOptions { + // set to true to turn off analysis progress reporting by lint + quiet false + // if true, stop the gradle build if errors are found + abortOnError false + // if true, only report errors + ignoreWarnings false + lintConfig file('lint.xml') } + } else { + throw new GradleException("Could not read version.properties!") } } @@ -74,15 +95,24 @@ android.applicationVariants.all { variant -> dependencies { compile fileTree(dir: 'libs', include: '*.jar') - compile 'com.android.support:appcompat-v7:19.+' - compile 'com.android.support:support-annotations:21.0.0' - compile 'com.google.android.gms:play-services:4.0.30' + compile 'com.android.support:appcompat-v7:25.0.1' + compile 'com.android.support:support-annotations:25.0.1' + compile 'com.google.android.gms:play-services-base:7.5.0' + compile 'com.google.android.gms:play-services-maps:7.5.0' + compile 'com.google.android.gms:play-services-gcm:7.5.0' compile 'org.ocpsoft.prettytime:prettytime:3.2.4.Final' - compile 'com.google.maps.android:android-maps-utils:0.3.3' + compile 'com.google.maps.android:android-maps-utils:0.4' compile 'com.fasterxml.jackson.core:jackson-databind:2.4.4' compile 'com.astuetz:pagerslidingtabstrip:1.0.1' - androidTestCompile 'junit:junit:4.12' - androidTestCompile 'org.mockito:mockito-core:1.9.5' + compile 'com.google.code.gson:gson:2.3' + + testCompile 'junit:junit:4.12' + testCompile 'org.mockito:mockito-core:1.10.19' + testCompile "org.powermock:powermock-module-junit4:1.6.2" + testCompile "org.powermock:powermock-module-junit4-rule:1.6.2" + testCompile "org.powermock:powermock-api-mockito:1.6.2" + testCompile "org.powermock:powermock-classloading-xstream:1.6.2" + androidTestCompile 'com.google.dexmaker:dexmaker:1.2' androidTestCompile 'com.google.dexmaker:dexmaker-mockito:1.2' } diff --git a/app/src/debug/res/values/donottranslate.xml b/app/src/debug/res/values/donottranslate.xml new file mode 100644 index 000000000..1caca3b45 --- /dev/null +++ b/app/src/debug/res/values/donottranslate.xml @@ -0,0 +1,4 @@ + + + AIzaSyBuLUoucTMjv2kcGSN3s6tYy6YFa-9WV94 + \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index b24da940a..0668a82e3 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -1,125 +1,126 @@ + package="org.akvo.flow"> - + + + + + + + + + + + + + - - - - - - - - - - - - - + + android:name=".app.FlowApp" + android:hasCode="true" + android:icon="@drawable/app_icon" + android:label="@string/app_name" + android:theme="@style/Flow.Theme"> + android:name=".activity.AddUserActivity" + android:label="@string/add_user" + android:configChanges="locale|layoutDirection"/> + android:name=".activity.SurveyActivity" + android:configChanges="locale|layoutDirection" + android:launchMode="singleTop"> - + - + - + + android:name="android.app.searchable" + android:resource="@xml/searchable"/> + android:name=".activity.RecordActivity" + android:configChanges="locale|layoutDirection"/> + android:name=".activity.FormActivity" + android:configChanges="keyboardHidden|orientation|screenSize|locale|layoutDirection" + android:windowSoftInputMode="adjustResize"/> + android:name=".activity.SettingsActivity" + android:configChanges="locale|layoutDirection" + android:label="@string/settingslabel"/> + android:name=".activity.PreferencesActivity" + android:configChanges="locale|layoutDirection" + android:label="@string/prefoptlabel"/> + android:name=".activity.TransmissionHistoryActivity" + android:configChanges="locale|layoutDirection"/> + android:name=".activity.GeoshapeActivity" + android:configChanges="locale|layoutDirection|orientation|screenSize"/> + android:name=".activity.AppUpdateActivity" + android:configChanges="locale|layoutDirection" + android:label="@string/app_update_activity" + android:launchMode="singleTop" + android:theme="@style/Flow.Dialog"/> - + android:name=".activity.TimeCheckActivity" + android:configChanges="locale|layoutDirection" + android:label="@string/time_check_activity" + android:launchMode="singleTop" + android:theme="@style/Flow.Dialog"/> + + android:name=".activity.SignatureActivity" + android:screenOrientation="landscape" + android:configChanges="locale|layoutDirection"/> - - - - - - - - - + + + + + + + + + + + + - - + + + android:name="com.google.android.geo.API_KEY" + android:value="@string/google_maps_key"/> + + android:name=".dao.DataProvider" + android:authorities="org.akvo.flow" + android:label="@string/app_name" + android:syncable="true" + android:writePermission="org.akvo.flow.permission.WRITE_SCHEDULE"/> - diff --git a/app/src/main/java/org/akvo/flow/activity/AddUserActivity.java b/app/src/main/java/org/akvo/flow/activity/AddUserActivity.java index 27213ca11..acc8935bb 100644 --- a/app/src/main/java/org/akvo/flow/activity/AddUserActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/AddUserActivity.java @@ -16,8 +16,8 @@ package org.akvo.flow.activity; +import android.app.Activity; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; import android.text.Editable; import android.text.TextUtils; import android.text.TextWatcher; @@ -33,7 +33,7 @@ import org.akvo.flow.domain.User; import org.akvo.flow.util.ConstantUtil; -public class AddUserActivity extends ActionBarActivity implements TextWatcher, TextView.OnEditorActionListener { +public class AddUserActivity extends Activity implements TextWatcher, TextView.OnEditorActionListener { private View mNextBt; private EditText mName; @@ -42,7 +42,7 @@ public class AddUserActivity extends ActionBarActivity implements TextWatcher, T @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - supportRequestWindowFeature(Window.FEATURE_NO_TITLE); + requestWindowFeature(Window.FEATURE_NO_TITLE); setContentView(R.layout.add_user_activity); mName = (EditText) findViewById(R.id.username); diff --git a/app/src/main/java/org/akvo/flow/activity/AppUpdateActivity.java b/app/src/main/java/org/akvo/flow/activity/AppUpdateActivity.java index 9b7b88efc..11c9cf88c 100644 --- a/app/src/main/java/org/akvo/flow/activity/AppUpdateActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/AppUpdateActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2014 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2014-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -13,6 +13,7 @@ * * The full license text can also be seen at . */ + package org.akvo.flow.activity; import android.app.Activity; @@ -32,7 +33,6 @@ import org.akvo.flow.util.FileUtil.FileType; import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.StatusUtil; -import org.apache.http.HttpStatus; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -57,7 +57,9 @@ public class AppUpdateActivity extends Activity { private ProgressBar mProgress; private UpdateAsyncTask mTask; - String mUrl, mVersion, mMd5Checksum; + private String mUrl; + private String mVersion; + private String mMd5Checksum; @Override protected void onCreate(Bundle savedInstanceState) { @@ -69,14 +71,14 @@ protected void onCreate(Bundle savedInstanceState) { mVersion = getIntent().getStringExtra(EXTRA_VERSION); mMd5Checksum = getIntent().getStringExtra(EXTRA_CHECKSUM); - mInstallBtn = (Button)findViewById(R.id.install_btn); - mProgress = (ProgressBar)findViewById(R.id.progress); + mInstallBtn = (Button) findViewById(R.id.install_btn); + mProgress = (ProgressBar) findViewById(R.id.progress); mProgress.setMax(MAX_PROGRESS);// Values will be in percentage // If the file is already downloaded, just prompt the install text final String filename = checkLocalFile(); if (filename != null) { - TextView updateTV = (TextView)findViewById(R.id.update_text); + TextView updateTV = (TextView) findViewById(R.id.update_text); updateTV.setText(R.string.clicktoinstall); mInstallBtn.setOnClickListener(new View.OnClickListener() { @Override @@ -95,7 +97,7 @@ public void onClick(View v) { }); } - Button cancelBtn = (Button)findViewById(R.id.cancel_btn); + Button cancelBtn = (Button) findViewById(R.id.cancel_btn); cancelBtn.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { @@ -111,7 +113,7 @@ public void onClick(View v) { * @return filename of the already downloaded file, if exists. Null otherwise */ private String checkLocalFile() { - final String latestVersion = FileUtil.checkDownloadedVersions(this); + final String latestVersion = FileUtil.checkDownloadedVersions(); if (latestVersion != null) { if (mMd5Checksum != null) { // The file was found, but we need to ensure the checksum matches, @@ -181,7 +183,8 @@ protected void onProgressUpdate(Integer... progress) { @Override protected void onPostExecute(String filename) { if (TextUtils.isEmpty(filename)) { - Toast.makeText(AppUpdateActivity.this, R.string.apk_upgrade_error, Toast.LENGTH_SHORT).show(); + Toast.makeText(AppUpdateActivity.this, R.string.apk_upgrade_error, + Toast.LENGTH_SHORT).show(); mInstallBtn.setText(R.string.retry); mInstallBtn.setEnabled(true); return; @@ -192,7 +195,7 @@ protected void onPostExecute(String filename) { } @Override - protected void onCancelled () { + protected void onCancelled() { Log.d(TAG, "onCancelled() - APK update task cancelled"); mProgress.setProgress(0); cleanupDownloads(mVersion); @@ -206,6 +209,7 @@ private void cleanupDownloads(String version) { /** * Wipe any existing apk file, and create a new File for the new one, according to the * given version + * * @param location * @param version * @return @@ -226,9 +230,6 @@ private File createFile(String location, String version) { * Downloads the apk file and stores it on the file system * After the download, a new notification will be displayed, requesting * the user to 'click to installAppUpdate' - * - * @param remoteFile - * @param surveyId */ private boolean downloadApk(String location, String localPath) { Log.i(TAG, "App Update: Downloading new version " + mVersion + " from " + mUrl); @@ -267,7 +268,7 @@ private boolean downloadApk(String location, String localPath) { final int status = conn.getResponseCode(); - if (status == HttpStatus.SC_OK) { + if (status == HttpURLConnection.HTTP_OK) { final String checksum = FileUtil.hexMd5(new File(localPath)); if (TextUtils.isEmpty(checksum)) { throw new IOException("Downloaded file is not available"); @@ -276,7 +277,8 @@ private boolean downloadApk(String location, String localPath) { if (mMd5Checksum == null) { // If we don't have a checksum yet, try to get it form the ETag header String etag = conn.getHeaderField("ETag"); - mMd5Checksum = etag != null ? etag.replaceAll("\"", "") : null;// Remove quotes + mMd5Checksum = + etag != null ? etag.replaceAll("\"", "") : null;// Remove quotes } // Compare the MD5, if found. Otherwise, rely on the 200 status code ok = mMd5Checksum == null || mMd5Checksum.equals(checksum); diff --git a/app/src/main/java/org/akvo/flow/activity/BackActivity.java b/app/src/main/java/org/akvo/flow/activity/BackActivity.java index 10164e7b0..11c02960c 100644 --- a/app/src/main/java/org/akvo/flow/activity/BackActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/BackActivity.java @@ -1,21 +1,37 @@ +/* + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + * + */ + package org.akvo.flow.activity; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatActivity; import android.view.MenuItem; -import org.akvo.flow.R; - -public abstract class BackActivity extends ActionBarActivity { +public abstract class BackActivity extends AppCompatActivity { @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - if (getSupportActionBar() != null) { - //getSupportActionBar().setDisplayHomeAsUpEnabled(true); - getSupportActionBar().setHomeButtonEnabled(true); - getSupportActionBar().setIcon(R.drawable.ic_arrow_back_white_48dp); + ActionBar supportActionBar = getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayHomeAsUpEnabled(true); + supportActionBar.setHomeButtonEnabled(true); } } diff --git a/app/src/main/java/org/akvo/flow/activity/FormActivity.java b/app/src/main/java/org/akvo/flow/activity/FormActivity.java index 3c0820037..b3e2ec843 100644 --- a/app/src/main/java/org/akvo/flow/activity/FormActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/FormActivity.java @@ -16,7 +16,6 @@ package org.akvo.flow.activity; -import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; @@ -27,6 +26,7 @@ import android.os.Environment; import android.os.StatFs; import android.support.v4.view.ViewPager; +import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.util.Log; import android.view.Menu; @@ -71,11 +71,11 @@ public class FormActivity extends BackActivity implements SurveyListener, private static final int PHOTO_ACTIVITY_REQUEST = 1; private static final int VIDEO_ACTIVITY_REQUEST = 2; - private static final int SCAN_ACTIVITY_REQUEST = 3; - private static final int EXTERNAL_SOURCE_REQUEST = 4; - private static final int CADDISFLY_REQUEST = 5; - private static final int PLOTTING_REQUEST = 6; - private static final int SIGNATURE_REQUEST = 7; + private static final int SCAN_ACTIVITY_REQUEST = 3; + private static final int EXTERNAL_SOURCE_REQUEST = 4; + private static final int CADDISFLY_REQUEST = 5; + private static final int PLOTTING_REQUEST = 6; + private static final int SIGNATURE_REQUEST = 7; private static final String TEMP_PHOTO_NAME_PREFIX = "image"; private static final String TEMP_VIDEO_NAME_PREFIX = "video"; @@ -112,14 +112,15 @@ protected void onCreate(Bundle savedInstanceState) { final String surveyId = getIntent().getStringExtra(ConstantUtil.SURVEY_ID_KEY); mReadOnly = getIntent().getBooleanExtra(ConstantUtil.READONLY_KEY, false); mSurveyInstanceId = getIntent().getLongExtra(ConstantUtil.RESPONDENT_ID_KEY, 0); - mSurveyGroup = (SurveyGroup)getIntent().getSerializableExtra(ConstantUtil.SURVEY_GROUP); + mSurveyGroup = (SurveyGroup) getIntent().getSerializableExtra(ConstantUtil.SURVEY_GROUP); mRecordId = getIntent().getStringExtra(ConstantUtil.SURVEYED_LOCALE_ID); - mQuestionResponses = new HashMap(); + mQuestionResponses = new HashMap<>(); mDatabase = new SurveyDbAdapter(this); mDatabase.open(); - loadSurvey(surveyId);// Load Survey. This task would be better off if executed in a worker thread + loadSurvey( + surveyId);// Load Survey. This task would be better off if executed in a worker thread loadLanguages(); if (mSurvey == null) { @@ -131,17 +132,18 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setTitle(mSurvey.getName()); getSupportActionBar().setSubtitle("v " + getVersion()); - mPager = (ViewPager)findViewById(R.id.pager); + mPager = (ViewPager) findViewById(R.id.pager); mAdapter = new SurveyTabAdapter(this, getSupportActionBar(), mPager, this, this); mPager.setAdapter(mAdapter); // Initialize new survey or load previous responses Map responses = mDatabase.getResponses(mSurveyInstanceId); if (!responses.isEmpty()) { - loadState(responses); + displayResponses(responses); } spaceLeftOnCard(); + Log.d(TAG, "form activity"); } /** @@ -181,7 +183,7 @@ private void prefillSurvey(long prefillSurveyInstance) { response.setId(null); response.setRespondentId(mSurveyInstanceId); } - loadState(responses); + displayResponses(responses); } private void loadSurvey(String surveyId) { @@ -197,7 +199,10 @@ private void loadSurvey(String surveyId) { Log.e(TAG, "Could not load survey xml file"); } finally { if (in != null) { - try { in.close(); } catch (IOException e) {} + try { + in.close(); + } catch (IOException e) { + } } } } @@ -220,15 +225,15 @@ private double getVersion() { /** * Load state for the current survey instance */ - private void loadState() { + private void loadResponses() { Map responses = mDatabase.getResponses(mSurveyInstanceId); - loadState(responses); + displayResponses(responses); } /** * Load state with the provided responses map */ - private void loadState(Map responses) { + private void displayResponses(Map responses) { mQuestionResponses = responses; mAdapter.reset();// Propagate the change } @@ -280,8 +285,10 @@ private void saveRecordMetaData() { if (!localeNameQuestions.isEmpty()) { boolean first = true; for (String questionId : localeNameQuestions) { - QuestionResponse questionResponse = mDatabase.getResponse(mSurveyInstanceId, questionId); - String answer = questionResponse != null ? questionResponse.getDatapointNameValue() : null; + QuestionResponse questionResponse = mDatabase + .getResponse(mSurveyInstanceId, questionId); + String answer = + questionResponse != null ? questionResponse.getDatapointNameValue() : null; if (!TextUtils.isEmpty(answer)) { if (!first) { @@ -293,7 +300,7 @@ private void saveRecordMetaData() { } // Make sure the value is not larger than 500 chars builder.setLength(Math.min(builder.length(), 500)); - + mDatabase.updateSurveyedLocale(mSurveyInstanceId, builder.toString(), SurveyedLocaleMeta.NAME); } @@ -384,7 +391,7 @@ private void clearSurvey() { @Override public void onClick(DialogInterface dialog, int which) { mDatabase.deleteResponses(String.valueOf(mSurveyInstanceId)); - loadState(); + loadResponses(); spaceLeftOnCard(); } }); @@ -398,7 +405,8 @@ private void displayLanguagesDialog() { final String[] langsSelectedNameArray = langsPrefData.getLangsSelectedNameArray(); final boolean[] langsSelectedBooleanArray = langsPrefData.getLangsSelectedBooleanArray(); - final int[] langsSelectedMasterIndexArray = langsPrefData.getLangsSelectedMasterIndexArray(); + final int[] langsSelectedMasterIndexArray = langsPrefData + .getLangsSelectedMasterIndexArray(); ViewUtil.displayLanguageSelector(this, langsSelectedNameArray, langsSelectedBooleanArray, @@ -429,7 +437,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { switch (requestCode) { case PHOTO_ACTIVITY_REQUEST: case VIDEO_ACTIVITY_REQUEST: - String fileSuffix = requestCode == PHOTO_ACTIVITY_REQUEST ? IMAGE_SUFFIX : VIDEO_SUFFIX; + String fileSuffix = + requestCode == PHOTO_ACTIVITY_REQUEST ? IMAGE_SUFFIX : VIDEO_SUFFIX; File tmp = getTmpFile(requestCode == PHOTO_ACTIVITY_REQUEST); // Ensure no image is saved in the DCIM folder @@ -444,7 +453,8 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { maxImgSize = Integer.valueOf(maxImgSizePref); } - if (ImageUtil.resizeImage(tmp.getAbsolutePath(), imgFile.getAbsolutePath(), maxImgSize)) { + if (ImageUtil.resizeImage(tmp.getAbsolutePath(), imgFile.getAbsolutePath(), + maxImgSize)) { Log.i(TAG, "Image resized to: " + getResources().getStringArray(R.array.max_image_size_pref)[maxImgSize]); if (!tmp.delete()) { // must check return value to know if it failed @@ -517,7 +527,8 @@ public void onSurveySubmit() { saveState(); // if we have no missing responses, submit the survey - mDatabase.updateSurveyStatus(mSurveyInstanceId, SurveyDbAdapter.SurveyInstanceStatus.SUBMITTED); + mDatabase.updateSurveyStatus(mSurveyInstanceId, + SurveyDbAdapter.SurveyInstanceStatus.SUBMITTED); // Make the current survey immutable mReadOnly = true; @@ -650,15 +661,16 @@ public void onClick(DialogInterface dialog, int id) { Intent intent = new Intent(ConstantUtil.EXTERNAL_SOURCE_ACTION); intent.putExtras(event.getData()); intent.setType(ConstantUtil.CADDISFLY_MIME); - startActivityForResult(Intent.createChooser(intent, getString(R.string.use_external_source)), - + EXTERNAL_SOURCE_REQUEST); + startActivityForResult( + Intent.createChooser(intent, getString(R.string.use_external_source)), + +EXTERNAL_SOURCE_REQUEST); } else if (QuestionInteractionEvent.CADDISFLY.equals(event.getEventType())) { mRequestQuestionId = event.getSource().getQuestion().getId(); Intent intent = new Intent(ConstantUtil.CADDISFLY_ACTION); intent.putExtras(event.getData()); intent.setType(ConstantUtil.CADDISFLY_MIME); startActivityForResult(Intent.createChooser(intent, getString(R.string.caddisfly_test)), - + CADDISFLY_REQUEST); + +CADDISFLY_REQUEST); } else if (QuestionInteractionEvent.PLOTTING_EVENT.equals(event.getEventType())) { Intent i = new Intent(this, GeoshapeActivity.class); if (event.getData() != null) { diff --git a/app/src/main/java/org/akvo/flow/activity/GeoshapeActivity.java b/app/src/main/java/org/akvo/flow/activity/GeoshapeActivity.java index 9334732ad..bafc0bfa2 100644 --- a/app/src/main/java/org/akvo/flow/activity/GeoshapeActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/GeoshapeActivity.java @@ -1,17 +1,21 @@ /* - * Copyright (C) 2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2015-2017 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo Flow. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo Flow is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo Flow 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 for more details. + * + * You should have received a copy of the GNU General Public License + * along with Akvo Flow. If not, see . * - * The full license text can also be seen at . */ package org.akvo.flow.activity; @@ -21,7 +25,8 @@ import android.graphics.Color; import android.location.Location; import android.os.Bundle; -import android.support.v7.app.ActionBarActivity; +import android.support.annotation.Nullable; +import android.support.v7.app.AppCompatActivity; import android.text.TextUtils; import android.util.Log; import android.view.Menu; @@ -34,9 +39,10 @@ import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.OnMapLongClickListener; -import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener; import com.google.android.gms.maps.GoogleMap.OnMarkerClickListener; +import com.google.android.gms.maps.GoogleMap.OnMarkerDragListener; import com.google.android.gms.maps.GoogleMap.OnMyLocationChangeListener; +import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.LatLng; import com.google.android.gms.maps.model.LatLngBounds; @@ -57,8 +63,10 @@ import java.util.ArrayList; import java.util.List; -public class GeoshapeActivity extends ActionBarActivity implements OnMapLongClickListener, - OnMarkerDragListener, OnMarkerClickListener, OnMyLocationChangeListener { +public class GeoshapeActivity extends AppCompatActivity + implements OnMapLongClickListener, OnMarkerDragListener, OnMarkerClickListener, + OnMyLocationChangeListener, OnMapReadyCallback { + private static final String JSON_TYPE = "type"; private static final String JSON_GEOMETRY = "geometry"; private static final String JSON_COORDINATES = "coordinates"; @@ -69,6 +77,7 @@ public class GeoshapeActivity extends ActionBarActivity implements OnMapLongClic private static final String TAG = GeoshapeActivity.class.getSimpleName(); private static final float ACCURACY_THRESHOLD = 20f; + public static final int MAP_ZOOM_LEVEL = 10; private List mFeatures;// Saved features private Feature mCurrentFeature;// Ongoing feature @@ -82,6 +91,8 @@ public class GeoshapeActivity extends ActionBarActivity implements OnMapLongClic private View mClearPointBtn; private TextView mFeatureName; private TextView mAccuracy; + + @Nullable private GoogleMap mMap; @Override @@ -92,13 +103,13 @@ protected void onCreate(Bundle savedInstanceState) { getSupportActionBar().setDisplayHomeAsUpEnabled(true); mFeatures = new ArrayList<>(); - mMap = ((SupportMapFragment)getSupportFragmentManager().findFragmentById(R.id.map)).getMap(); + ((SupportMapFragment) getSupportFragmentManager().findFragmentById(R.id.map)).getMapAsync(this); View addPointBtn = findViewById(R.id.add_point_btn); View clearFeatureBtn = findViewById(R.id.clear_feature_btn); mFeatureMenu = findViewById(R.id.feature_menu); - mFeatureName = (TextView)findViewById(R.id.feature_name); - mAccuracy = (TextView)findViewById(R.id.accuracy); + mFeatureName = (TextView) findViewById(R.id.feature_name); + mAccuracy = (TextView) findViewById(R.id.accuracy); mClearPointBtn = findViewById(R.id.clear_point_btn); findViewById(R.id.properties).setOnClickListener(mFeatureMenuListener); @@ -117,14 +128,13 @@ protected void onCreate(Bundle savedInstanceState) { addPointBtn.setVisibility(View.GONE); clearFeatureBtn.setVisibility(View.GONE); } + } + @Override + public void onMapReady(GoogleMap googleMap) { + mMap = googleMap; initMap(); - - String geoJSON = getIntent().getStringExtra(ConstantUtil.GEOSHAPE_RESULT); - if (!TextUtils.isEmpty(geoJSON)) { - load(geoJSON); - mCentered = true; - } + updateMapCenter(); } private void initMap() { @@ -139,6 +149,16 @@ private void initMap() { } } + private void updateMapCenter() { + if (mMap != null) { + String geoJSON = getIntent().getStringExtra(ConstantUtil.GEOSHAPE_RESULT); + if (!TextUtils.isEmpty(geoJSON)) { + load(geoJSON); + mCentered = true; + } + } + } + private void selectFeature(Feature feature, Marker marker) { // Remove current selection, if any if (mCurrentFeature != null && mCurrentFeature != feature) { @@ -226,7 +246,6 @@ public boolean onOptionsItemSelected(MenuItem item) { @Override public void onResume() { super.onResume(); - } private View.OnClickListener mFeatureMenuListener = new View.OnClickListener() { @@ -238,36 +257,34 @@ public void onClick(View v) { switch (v.getId()) { case R.id.add_point_btn: - Location location = mMap.getMyLocation(); + Location location = mMap == null? null : mMap.getMyLocation(); if (location != null && location.getAccuracy() <= ACCURACY_THRESHOLD) { addPoint(new LatLng(location.getLatitude(), location.getLongitude())); } else { Toast.makeText(GeoshapeActivity.this, - location != null ? R.string.location_inaccurate : R.string.location_unknown, - Toast.LENGTH_LONG).show(); + location != null ? R.string.location_inaccurate : R.string.location_unknown, + Toast.LENGTH_LONG).show(); } break; case R.id.clear_point_btn: - ViewUtil.showConfirmDialog(R.string.clear_point_title, - R.string.clear_point_text, GeoshapeActivity.this, true, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mCurrentFeature.removePoint(); - selectFeature(mCurrentFeature, null); - } - }); + ViewUtil.showConfirmDialog(R.string.clear_point_title, R.string.clear_point_text, + GeoshapeActivity.this, true, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mCurrentFeature.removePoint(); + selectFeature(mCurrentFeature, null); + } + }); break; case R.id.clear_feature_btn: - ViewUtil.showConfirmDialog(R.string.clear_feature_title, - R.string.clear_feature_text, GeoshapeActivity.this, true, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - mCurrentFeature.delete(); - selectFeature(null, null); - } - }); + ViewUtil.showConfirmDialog(R.string.clear_feature_title, R.string.clear_feature_text, + GeoshapeActivity.this, true, new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + mCurrentFeature.delete(); + selectFeature(null, null); + } + }); break; case R.id.properties: displayProperties(); @@ -277,16 +294,12 @@ public void onClick(DialogInterface dialog, int which) { }; private void displayProperties() { - final ArrayAdapter adapter = new ArrayAdapter<>(this, - android.R.layout.simple_dropdown_item_1line); + final ArrayAdapter adapter = new ArrayAdapter<>(this, android.R.layout.simple_dropdown_item_1line); for (Feature.Property property : mCurrentFeature.getProperties()) { adapter.add(String.format("%s: %s", property.mDisplayName, property.mDisplayValue)); } - new AlertDialog.Builder(GeoshapeActivity.this) - .setTitle("Properties") - .setAdapter(adapter, null) - .show(); + new AlertDialog.Builder(GeoshapeActivity.this).setTitle("Properties").setAdapter(adapter, null).show(); } /** @@ -360,7 +373,7 @@ private void load(String geoJSON) { if (jFeatures == null || jFeatures.length() == 0) { return; } - for (int i=0; i points = new ArrayList<>(); - for (int j=0; j LatLng(lat, lon) + LatLng point = + new LatLng(jPoint.getDouble(1), jPoint.getDouble(0));// [lon, lat] -> LatLng(lat, lon) points.add(point); builder.include(point); } @@ -416,14 +430,13 @@ public void onMapLongClick(final LatLng latLng) { if (mCurrentFeature == null) { return; } - ViewUtil.showConfirmDialog(R.string.add_point_title, - R.string.add_point_text, GeoshapeActivity.this, true, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - addPoint(latLng); - } - }); + ViewUtil.showConfirmDialog(R.string.add_point_title, R.string.add_point_text, GeoshapeActivity.this, true, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + addPoint(latLng); + } + }); } @Override @@ -465,8 +478,8 @@ public void onMarkerDragEnd(Marker marker) { public void onMyLocationChange(Location location) { Log.i(TAG, "onMyLocationChange() - " + location); if (location != null && location.hasAccuracy()) { - mAccuracy.setText(getString(R.string.accuracy) + ": " - + new DecimalFormat("#").format(location.getAccuracy()) + "m"); + mAccuracy.setText( + getString(R.string.accuracy) + ": " + new DecimalFormat("#").format(location.getAccuracy()) + "m"); if (location.getAccuracy() <= ACCURACY_THRESHOLD) { mAccuracy.setTextColor(getResources().getColor(R.color.button_green)); } else { @@ -474,9 +487,15 @@ public void onMyLocationChange(Location location) { } if (!mCentered) { LatLng position = new LatLng(location.getLatitude(), location.getLongitude()); - mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, 10)); - mCentered = true; + centerMapOnLocation(position); } } } + + private void centerMapOnLocation(LatLng position) { + if (mMap != null) { + mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, MAP_ZOOM_LEVEL)); + mCentered = true; + } + } } diff --git a/app/src/main/java/org/akvo/flow/activity/PreferencesActivity.java b/app/src/main/java/org/akvo/flow/activity/PreferencesActivity.java index 2c54ebe34..633862cc0 100644 --- a/app/src/main/java/org/akvo/flow/activity/PreferencesActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/PreferencesActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -16,8 +16,6 @@ package org.akvo.flow.activity; -import java.util.HashMap; - import android.app.AlertDialog; import android.content.DialogInterface; import android.content.Intent; @@ -27,24 +25,25 @@ import android.view.View.OnClickListener; import android.widget.CheckBox; import android.widget.CompoundButton; +import android.widget.CompoundButton.OnCheckedChangeListener; import android.widget.EditText; import android.widget.TextView; -import android.widget.CompoundButton.OnCheckedChangeListener; import org.akvo.flow.R; import org.akvo.flow.app.FlowApp; import org.akvo.flow.dao.SurveyDbAdapter; -import org.akvo.flow.service.LocationService; import org.akvo.flow.service.SurveyDownloadService; import org.akvo.flow.util.ArrayPreferenceUtil; import org.akvo.flow.util.ConstantUtil; +import org.akvo.flow.util.LangsPreferenceData; +import org.akvo.flow.util.LangsPreferenceUtil; import org.akvo.flow.util.PropertyUtil; import org.akvo.flow.util.StatusUtil; import org.akvo.flow.util.StringUtil; -import org.akvo.flow.util.LangsPreferenceData; -import org.akvo.flow.util.LangsPreferenceUtil; import org.akvo.flow.util.ViewUtil; +import java.util.HashMap; + /** * Displays user editable preferences and takes care of persisting them to the * database. Some options require the user to enter an administrator passcode @@ -54,7 +53,6 @@ */ public class PreferencesActivity extends BackActivity implements OnClickListener, OnCheckedChangeListener { - private CheckBox beaconCheckbox; private CheckBox screenOnCheckbox; private CheckBox mobileDataCheckbox; private TextView languageTextView; @@ -77,7 +75,6 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.preferences); - beaconCheckbox = (CheckBox) findViewById(R.id.beaconcheckbox); screenOnCheckbox = (CheckBox) findViewById(R.id.screenoptcheckbox); mobileDataCheckbox = (CheckBox) findViewById(R.id.uploadoptioncheckbox); languageTextView = (TextView) findViewById(R.id.surveylangvalue); @@ -92,7 +89,6 @@ public void onCreate(Bundle savedInstanceState) { maxImgSizes = res.getStringArray(R.array.max_image_size_pref); // Setup event listeners - beaconCheckbox.setOnCheckedChangeListener(this); screenOnCheckbox.setOnCheckedChangeListener(this); mobileDataCheckbox.setOnCheckedChangeListener(this); findViewById(R.id.pref_locale).setOnClickListener(this); @@ -114,13 +110,6 @@ private void populateFields() { screenOnCheckbox.setChecked(false); } - val = settings.get(ConstantUtil.LOCATION_BEACON_SETTING_KEY); - if (val != null && Boolean.parseBoolean(val)) { - beaconCheckbox.setChecked(true); - } else { - beaconCheckbox.setChecked(false); - } - val = settings.get(ConstantUtil.CELL_UPLOAD_SETTING_KEY); mobileDataCheckbox.setChecked(val != null && Boolean.parseBoolean(val)); @@ -208,29 +197,30 @@ public void onClick(DialogInterface dialog, int clicked) { break; case R.id.pref_server: ViewUtil.showAdminAuthDialog(this, new ViewUtil.AdminAuthDialogListener() { - @Override - public void onAuthenticated() { - final EditText inputView = new EditText(PreferencesActivity.this); - // one line only - inputView.setSingleLine(); - inputView.setText(StatusUtil.getServerBase(PreferencesActivity.this)); - ViewUtil.ShowTextInputDialog( - PreferencesActivity.this, R.string.serverlabel, - R.string.serverlabel, inputView, - new DialogInterface.OnClickListener() { - @Override - public void onClick(DialogInterface dialog, int which) { - String s = StringUtil.ControlToSPace(inputView - .getText().toString()); - // drop any control chars, especially tabs - database.savePreference( - ConstantUtil.SERVER_SETTING_KEY, s); - serverTextView.setText(StatusUtil.getServerBase(PreferencesActivity.this)); + @Override + public void onAuthenticated() { + final EditText inputView = new EditText(PreferencesActivity.this); + // one line only + inputView.setSingleLine(); + inputView.setText(StatusUtil.getServerBase(PreferencesActivity.this)); + ViewUtil.ShowTextInputDialog( + PreferencesActivity.this, R.string.serverlabel, + R.string.serverlabel, inputView, + new DialogInterface.OnClickListener() { + @Override + public void onClick(DialogInterface dialog, int which) { + String s = StringUtil.controlToSpace(inputView + .getText().toString()); + // drop any control chars, especially tabs + database.savePreference( + ConstantUtil.SERVER_SETTING_KEY, s); + serverTextView.setText( + StatusUtil.getServerBase(PreferencesActivity.this)); + } } - } - ); + ); + } } - } ); break; case R.id.pref_deviceid: @@ -248,7 +238,7 @@ public void onAuthenticated() { new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { - String s = StringUtil.ControlToSPace(inputView + String s = StringUtil.controlToSpace(inputView .getText().toString()); // drop any control chars, // especially tabs @@ -311,15 +301,7 @@ public void onClick(DialogInterface dialog, int which) { */ @Override public void onCheckedChanged(CompoundButton buttonView, boolean isChecked) { - if (buttonView == beaconCheckbox) { - database.savePreference(ConstantUtil.LOCATION_BEACON_SETTING_KEY, "" + isChecked); - if (isChecked) { - // if the option changed, kick the service so it reflects the change - startService(new Intent(this, LocationService.class)); - } else { - stopService(new Intent(this, LocationService.class)); - } - } else if (buttonView == screenOnCheckbox) { + if (buttonView == screenOnCheckbox) { database.savePreference(ConstantUtil.SCREEN_ON_KEY, "" + isChecked); } else if (buttonView == mobileDataCheckbox) { database.savePreference(ConstantUtil.CELL_UPLOAD_SETTING_KEY, "" + isChecked); diff --git a/app/src/main/java/org/akvo/flow/activity/RecordActivity.java b/app/src/main/java/org/akvo/flow/activity/RecordActivity.java index 0bb68e1f6..37eeabd2c 100644 --- a/app/src/main/java/org/akvo/flow/activity/RecordActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/RecordActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2013-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -49,9 +49,7 @@ public class RecordActivity extends BackActivity implements SurveyListListener, RecordListListener { public static final String EXTRA_SURVEY_GROUP = "survey_group"; public static final String EXTRA_RECORD_ID = "record"; - - //private static final String TAG = RecordActivity.class.getSimpleName(); - + private static final int POSITION_SURVEYS = 0; private static final int POSITION_RESPONSES = 1; @@ -61,18 +59,18 @@ public class RecordActivity extends BackActivity implements SurveyListListener, private SurveyedLocale mRecord; private SurveyGroup mSurveyGroup; private SurveyDbAdapter mDatabase; - + private ViewPager mPager; private String[] mTabs; - + @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.record_activity); - + mTabs = getResources().getStringArray(R.array.record_tabs); - mPager = (ViewPager)findViewById(R.id.pager); + mPager = (ViewPager) findViewById(R.id.pager); mPager.setAdapter(new TabsAdapter(getSupportFragmentManager())); mPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override @@ -80,15 +78,16 @@ public void onPageSelected(int position) { getSupportActionBar().setSelectedNavigationItem(position); } }); - + mDatabase = new SurveyDbAdapter(this); - + mSurveyGroup = (SurveyGroup) getIntent().getSerializableExtra(EXTRA_SURVEY_GROUP); setTitle(mSurveyGroup.getName()); - + setupActionBar(); } - + + //TODO: replace deprecated Tabs private void setupActionBar() { final ActionBar actionBar = getSupportActionBar(); actionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); @@ -103,7 +102,7 @@ private void setupActionBar() { actionBar.addTab(listTab); actionBar.addTab(responsesTab); } - + @Override public void onResume() { super.onResume(); @@ -148,8 +147,11 @@ public void onSurveyClick(final String surveyId) { // Check if there are saved (non-submitted) responses for this Survey, and take the 1st one long[] instances = mDatabase.getFormInstances(mRecord.getId(), surveyId, SurveyInstanceStatus.SAVED); - long instance = instances.length > 0 ? instances[0] - : mDatabase.createSurveyRespondent(surveyId, survey.getVersion(), mUser, mRecord.getId()); + long instance = instances.length > 0 ? + instances[0] + : + mDatabase.createSurveyRespondent(surveyId, survey.getVersion(), mUser, + mRecord.getId()); Intent i = new Intent(this, FormActivity.class); i.putExtra(ConstantUtil.USER_ID_KEY, mUser.getId()); @@ -161,7 +163,7 @@ public void onSurveyClick(final String surveyId) { } class TabsAdapter extends FragmentPagerAdapter { - + public TabsAdapter(FragmentManager fm) { super(fm); } @@ -170,30 +172,30 @@ public TabsAdapter(FragmentManager fm) { public int getCount() { return mTabs.length; } - + @Override public Fragment getItem(int position) { switch (position) { case POSITION_SURVEYS: - return FormListFragment.instantiate(mSurveyGroup, mRecord); + return FormListFragment.newInstance(mSurveyGroup, mRecord); case POSITION_RESPONSES: return ResponseListFragment.instantiate(mSurveyGroup, mRecord); } - + return null; } - + @Override public CharSequence getPageTitle(int position) { return mTabs[position]; } - + } - + // ==================================== // // =========== Options Menu =========== // // ==================================== // - + @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.datapoint_activity, menu); @@ -211,7 +213,7 @@ public boolean onOptionsItemSelected(MenuItem item) { return super.onOptionsItemSelected(item); } } - + @Override public void onTabReselected(Tab tab, FragmentTransaction fragmentTransaction) { } diff --git a/app/src/main/java/org/akvo/flow/activity/SettingsActivity.java b/app/src/main/java/org/akvo/flow/activity/SettingsActivity.java index f609d5dd2..8998bbe5c 100644 --- a/app/src/main/java/org/akvo/flow/activity/SettingsActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/SettingsActivity.java @@ -39,11 +39,8 @@ import android.widget.SimpleAdapter; import android.widget.TextView; import android.widget.Toast; -import java.io.File; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; + +import org.akvo.flow.BuildConfig; import org.akvo.flow.R; import org.akvo.flow.app.FlowApp; import org.akvo.flow.async.ClearDataAsyncTask; @@ -55,6 +52,12 @@ import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.ViewUtil; +import java.io.File; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + /** * Displays the settings menu and handles the user choices * @@ -66,33 +69,43 @@ public class SettingsActivity extends BackActivity implements AdapterView.OnItem private static final String LABEL = "label"; private static final String DESC = "desc"; + //TODO: this will be replaced by a year placed in a properties file + private static final String CURRENT_YEAR = "2017"; + public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.settingsmenu); ArrayList> list = new ArrayList<>(); Resources resources = getResources(); - list.add(createMap(resources.getString(R.string.prefoptlabel), resources.getString(R.string.prefoptdesc))); - list.add(createMap(resources.getString(R.string.sendoptlabel), resources.getString(R.string.sendoptdesc))); + list.add(createMap(resources.getString(R.string.prefoptlabel), + resources.getString(R.string.prefoptdesc))); + list.add(createMap(resources.getString(R.string.sendoptlabel), + resources.getString(R.string.sendoptdesc))); list.add(createMap(resources.getString(R.string.reloadsurveyslabel), - resources.getString(R.string.reloadsurveysdesc))); + resources.getString(R.string.reloadsurveysdesc))); list.add(createMap(resources.getString(R.string.downloadsurveylabel), - resources.getString(R.string.downloadsurveydesc))); - list.add(createMap(resources.getString(R.string.poweroptlabel), resources.getString(R.string.poweroptdesc))); - list.add(createMap(resources.getString(R.string.gpsstatuslabel), resources.getString(R.string.gpsstatusdesc))); + resources.getString(R.string.downloadsurveydesc))); + list.add(createMap(resources.getString(R.string.poweroptlabel), + resources.getString(R.string.poweroptdesc))); + list.add(createMap(resources.getString(R.string.gpsstatuslabel), + resources.getString(R.string.gpsstatusdesc))); list.add(createMap(resources.getString(R.string.reset_responses), - resources.getString(R.string.reset_responses_desc))); - list.add(createMap(resources.getString(R.string.resetall), resources.getString(R.string.resetalldesc))); - list.add(createMap(resources.getString(R.string.checksd), resources.getString(R.string.checksddesc))); + resources.getString(R.string.reset_responses_desc))); + list.add(createMap(resources.getString(R.string.resetall), + resources.getString(R.string.resetalldesc))); + list.add(createMap(resources.getString(R.string.checksd), + resources.getString(R.string.checksddesc))); list.add(createMap(resources.getString(R.string.settings_app_update_title), - resources.getString(R.string.settings_app_update_description))); - list.add(createMap(resources.getString(R.string.aboutlabel), resources.getString(R.string.aboutdesc))); + resources.getString(R.string.settings_app_update_description))); + list.add(createMap(resources.getString(R.string.aboutlabel), + resources.getString(R.string.aboutdesc))); String[] fromKeys = { - LABEL, DESC + LABEL, DESC }; int[] toIds = { - R.id.optionLabel, R.id.optionDesc + R.id.optionLabel, R.id.optionDesc }; ListView lv = (ListView) findViewById(android.R.id.list); @@ -161,7 +174,8 @@ private void onCheckSdCardStateOptionTap(Resources resources) { String state = Environment.getExternalStorageState(); StringBuilder builder = new StringBuilder(); if (state == null || !Environment.MEDIA_MOUNTED.equals(state)) { - builder.append("").append(resources.getString(R.string.sdmissing)).append("
"); + builder.append("").append(resources.getString(R.string.sdmissing)) + .append("
"); } else { builder.append(resources.getString(R.string.sdmounted)).append("
"); File f = Environment.getExternalStorageDirectory(); @@ -177,7 +191,7 @@ private void onCheckSdCardStateOptionTap(Resources resources) { long bs = fs.getBlockSize(); long space = fb * bs; builder.append(resources.getString(R.string.sdcardspace)) - .append(String.format(" %.2f", (double) space / (double) (1024 * 1024))); + .append(String.format(" %.2f", (double) space / (double) (1024 * 1024))); } } AlertDialog.Builder dialog = new AlertDialog.Builder(this); @@ -233,8 +247,9 @@ public void onClick(DialogInterface dialog, int whichButton) { database.reinstallTestSurvey(); database.close(); } else if (!TextUtils.isEmpty(value)) { - Intent i = new Intent(SettingsActivity.this, SurveyDownloadService.class); - i.putExtra(SurveyDownloadService.EXTRA_SURVEYS, new String[] {value}); + Intent i = new Intent(SettingsActivity.this, + SurveyDownloadService.class); + i.putExtra(SurveyDownloadService.EXTRA_SURVEYS, new String[] { value }); SettingsActivity.this.startService(i); } } @@ -271,11 +286,12 @@ public void onClick(DialogInterface dialog, int id) { c.startService(i); } }); - builder.setNegativeButton(R.string.cancelbutton, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); + builder.setNegativeButton(R.string.cancelbutton, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); builder.show(); } }); @@ -283,7 +299,8 @@ public void onClick(DialogInterface dialog, int id) { private void onAboutOptionTap(Resources resources) { AlertDialog.Builder builder = new AlertDialog.Builder(this); - String txt = resources.getString(R.string.abouttext) + " " + PlatformUtil.getVersionName(this); + String txt = resources + .getString(R.string.about_text, CURRENT_YEAR, BuildConfig.VERSION_NAME); builder.setTitle(R.string.abouttitle); builder.setMessage(txt); builder.setPositiveButton(R.string.okbutton, new DialogInterface.OnClickListener() { @@ -342,7 +359,7 @@ private boolean unsentData() throws SQLException { * operation. * * @param responsesOnly Flag to specify a partial deletion (user generated - * data). + * data). */ private void deleteData(final boolean responsesOnly) throws SQLException { try { @@ -357,21 +374,22 @@ private void deleteData(final boolean responsesOnly) throws SQLException { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(messageId) - .setCancelable(true) - .setPositiveButton(R.string.okbutton, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - if (!responsesOnly) { - // Delete everything implies logging the current user out (if any) - FlowApp.getApp().setUser(null); - } - new ClearDataAsyncTask(SettingsActivity.this).execute(responsesOnly); - } - }) - .setNegativeButton(R.string.cancelbutton, new DialogInterface.OnClickListener() { - public void onClick(DialogInterface dialog, int id) { - dialog.cancel(); - } - }); + .setCancelable(true) + .setPositiveButton(R.string.okbutton, new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + if (!responsesOnly) { + // Delete everything implies logging the current user out (if any) + FlowApp.getApp().setUser(null); + } + new ClearDataAsyncTask(SettingsActivity.this).execute(responsesOnly); + } + }) + .setNegativeButton(R.string.cancelbutton, + new DialogInterface.OnClickListener() { + public void onClick(DialogInterface dialog, int id) { + dialog.cancel(); + } + }); builder.show(); } catch (SQLException e) { Log.e(TAG, e.getMessage()); @@ -392,8 +410,9 @@ public boolean onOptionsItemSelected(MenuItem item) { private static class SettingsAdapter extends SimpleAdapter { - public SettingsAdapter(Context context, List> data, int resource, String[] from, - int[] to) { + public SettingsAdapter(Context context, List> data, int resource, + String[] from, + int[] to) { super(context, data, resource, from, to); } diff --git a/app/src/main/java/org/akvo/flow/activity/SurveyActivity.java b/app/src/main/java/org/akvo/flow/activity/SurveyActivity.java index c6dd98f49..9f0e62914 100644 --- a/app/src/main/java/org/akvo/flow/activity/SurveyActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/SurveyActivity.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2016 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -24,9 +24,13 @@ import android.content.res.Configuration; import android.database.Cursor; import android.os.Bundle; -import android.support.v4.app.ActionBarDrawerToggle; +import android.support.v4.app.FragmentManager; +import android.support.v4.view.GravityCompat; import android.support.v4.widget.DrawerLayout; +import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; +import android.support.v7.app.ActionBarDrawerToggle; +import android.support.v7.graphics.drawable.DrawerArrowDrawable; import android.util.Log; import android.view.Gravity; import android.view.Menu; @@ -34,37 +38,43 @@ import android.view.View; import android.widget.Toast; +import org.akvo.flow.BuildConfig; import org.akvo.flow.R; import org.akvo.flow.app.FlowApp; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.domain.Survey; import org.akvo.flow.domain.SurveyGroup; import org.akvo.flow.domain.User; -import org.akvo.flow.service.ApkUpdateService; +import org.akvo.flow.domain.apkupdate.ApkUpdateStore; +import org.akvo.flow.domain.apkupdate.GsonMapper; +import org.akvo.flow.domain.apkupdate.ViewApkData; import org.akvo.flow.service.BootstrapService; import org.akvo.flow.service.DataSyncService; import org.akvo.flow.service.ExceptionReportingService; -import org.akvo.flow.service.LocationService; import org.akvo.flow.service.SurveyDownloadService; +import org.akvo.flow.service.SurveyedDataPointSyncService; import org.akvo.flow.service.TimeCheckService; +import org.akvo.flow.ui.Navigator; import org.akvo.flow.ui.fragment.DatapointsFragment; -import org.akvo.flow.ui.fragment.RecordListListener; import org.akvo.flow.ui.fragment.DrawerFragment; +import org.akvo.flow.ui.fragment.RecordListListener; import org.akvo.flow.util.ConstantUtil; +import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.Prefs; import org.akvo.flow.util.StatusUtil; import org.akvo.flow.util.ViewUtil; +import java.lang.ref.WeakReference; + public class SurveyActivity extends ActionBarActivity implements RecordListListener, - DrawerFragment.DrawerListener { + DrawerFragment.DrawerListener, DatapointsFragment.DatapointFragmentListener { private static final String TAG = SurveyActivity.class.getSimpleName(); - private static final int REQUEST_ADD_USER = 0; - // Argument to be passed to list/map fragments public static final String EXTRA_SURVEY_GROUP = "survey_group"; - public static final String FRAGMENT_DATAPOINTS = "datapoints_fragment"; + private static final String DATA_POINTS_FRAGMENT_TAG = "datapoints_fragment"; + private static final String DRAWER_FRAGMENT_TAG = "f"; private SurveyDbAdapter mDatabase; private SurveyGroup mSurveyGroup; @@ -73,12 +83,24 @@ public class SurveyActivity extends ActionBarActivity implements RecordListListe private ActionBarDrawerToggle mDrawerToggle; private DrawerFragment mDrawer; private CharSequence mDrawerTitle, mTitle; + private Navigator navigator = new Navigator(); + + private Prefs prefs; + private ApkUpdateStore apkUpdateStore; + + /** + * BroadcastReceiver to notify of surveys synchronisation. This should be + * fired from {@link SurveyDownloadService}. + */ + private final BroadcastReceiver mSurveysSyncReceiver = new SurveySyncBroadcastReceiver(this); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.survey_activity); + initializeToolBar(); + mDatabase = new SurveyDbAdapter(this); mDatabase.open(); @@ -87,50 +109,63 @@ protected void onCreate(Bundle savedInstanceState) { mTitle = mDrawerTitle = getString(R.string.app_name); // Init navigation drawer - mDrawer = (DrawerFragment)getSupportFragmentManager().findFragmentByTag("f"); + FragmentManager supportFragmentManager = getSupportFragmentManager(); + mDrawer = (DrawerFragment) supportFragmentManager.findFragmentByTag(DRAWER_FRAGMENT_TAG); mDrawerToggle = new ActionBarDrawerToggle(this, mDrawerLayout, - R.drawable.ic_drawer, R.string.drawer_open, R.string.drawer_close) { + R.string.drawer_open, R.string.drawer_close) { /** Called when a drawer has settled in a completely closed state. */ - public void onDrawerClosed(View view) { - super.onDrawerClosed(view); + @Override + public void onDrawerClosed(View drawerView) { + super.onDrawerClosed(drawerView); mDrawer.onDrawerClosed(); getSupportActionBar().setTitle(mTitle); supportInvalidateOptionsMenu(); } - /** Called when a drawer has settled in a completely open state. */ + /** + * Called when a drawer has settled in a completely open state. + */ + @Override public void onDrawerOpened(View drawerView) { super.onDrawerOpened(drawerView); + //prevent the back icon from showing + super.onDrawerSlide(drawerView, 0); getSupportActionBar().setTitle(mDrawerTitle); supportInvalidateOptionsMenu(); } + + @Override + public void onDrawerSlide(View drawerView, float slideOffset) { + //disable drawer animation + super.onDrawerSlide(drawerView, 0); + } }; - mDrawerLayout.setDrawerListener(mDrawerToggle); - getSupportActionBar().setDisplayHomeAsUpEnabled(false); - getSupportActionBar().setHomeButtonEnabled(true); - getSupportActionBar().setIcon(R.drawable.ic_menu_white_48dp); + mDrawerLayout.addDrawerListener(mDrawerToggle); // Automatically select the survey SurveyGroup sg = mDatabase.getSurveyGroup(FlowApp.getApp().getSurveyGroupId()); if (sg != null) { onSurveySelected(sg); } else { - mDrawerLayout.openDrawer(Gravity.START); + mDrawerLayout.openDrawer(GravityCompat.START); } - if (savedInstanceState == null || - getSupportFragmentManager().findFragmentByTag(FRAGMENT_DATAPOINTS) == null) { - getSupportFragmentManager().beginTransaction().replace(R.id.content_frame, - DatapointsFragment.instantiate(mSurveyGroup), FRAGMENT_DATAPOINTS).commit(); + if (savedInstanceState == null + || supportFragmentManager.findFragmentByTag(DATA_POINTS_FRAGMENT_TAG) == null) { + DatapointsFragment datapointsFragment = DatapointsFragment.newInstance(mSurveyGroup); + supportFragmentManager.beginTransaction() + .replace(R.id.content_frame, datapointsFragment, DATA_POINTS_FRAGMENT_TAG) + .commit(); } + prefs = new Prefs(getApplicationContext()); // Start the setup Activity if necessary. boolean noDevIdYet = false; - if (!Prefs.getBoolean(this, Prefs.KEY_SETUP, false)) { + if (!prefs.getBoolean(Prefs.KEY_SETUP, false)) { noDevIdYet = true; - startActivityForResult(new Intent(this, AddUserActivity.class), REQUEST_ADD_USER); + navigator.navigateToAddUser(this); } startServices(noDevIdYet); @@ -139,22 +174,30 @@ public void onDrawerOpened(View drawerView) { if (savedInstanceState == null) { displaySelectedUser(); } + apkUpdateStore = new ApkUpdateStore(new GsonMapper(), prefs); + } + + private void initializeToolBar() { + ActionBar supportActionBar = getSupportActionBar(); + if (supportActionBar != null) { + supportActionBar.setDisplayHomeAsUpEnabled(true); + } } @Override - protected void onActivityResult (int requestCode, int resultCode, Intent data) { + protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { - case REQUEST_ADD_USER: + case ConstantUtil.REQUEST_ADD_USER: if (resultCode == RESULT_OK) { displaySelectedUser(); - Prefs.setBoolean(this, Prefs.KEY_SETUP, true); + prefs.setBoolean(Prefs.KEY_SETUP, true); // Trigger the delayed services, so the first // backend connections uses the new Device ID startService(new Intent(this, SurveyDownloadService.class)); startService(new Intent(this, DataSyncService.class)); - } else if (!Prefs.getBoolean(this, Prefs.KEY_SETUP, false)) { + } else if (!prefs.getBoolean(Prefs.KEY_SETUP, false)) { finish(); } break; @@ -169,8 +212,16 @@ public void onResume() { mDatabase.deleteEmptyRecords(); registerReceiver(mSurveysSyncReceiver, new IntentFilter(getString(R.string.action_surveys_sync))); + + ViewApkData apkData = apkUpdateStore.getApkData(); + boolean shouldNotifyUpdate = apkUpdateStore.shouldNotifyNewVersion(); + if (apkData != null && shouldNotifyUpdate && PlatformUtil + .isNewerVersion(BuildConfig.VERSION_NAME, apkData.getVersion())) { + apkUpdateStore.saveAppUpdateNotifiedTime(); + navigator.navigateToAppUpdate(this, apkData); + } } - + @Override public void onPause() { super.onPause(); @@ -212,10 +263,8 @@ public void onClick(DialogInterface dialog, int which) { startService(new Intent(this, SurveyDownloadService.class)); startService(new Intent(this, DataSyncService.class)); } - startService(new Intent(this, LocationService.class)); startService(new Intent(this, BootstrapService.class)); startService(new Intent(this, ExceptionReportingService.class)); - startService(new Intent(this, ApkUpdateService.class)); startService(new Intent(this, TimeCheckService.class)); } } @@ -247,11 +296,13 @@ public void onSurveySelected(SurveyGroup surveyGroup) { setTitle(title); FlowApp.getApp().setSurveyGroupId(id); - DatapointsFragment f = (DatapointsFragment) getSupportFragmentManager().findFragmentByTag(FRAGMENT_DATAPOINTS); + DatapointsFragment f = (DatapointsFragment) getSupportFragmentManager().findFragmentByTag( + DATA_POINTS_FRAGMENT_TAG); if (f != null) { f.refresh(mSurveyGroup); + } else { + supportInvalidateOptionsMenu(); } - supportInvalidateOptionsMenu(); mDrawer.load(); mDrawerLayout.closeDrawers(); } @@ -262,15 +313,10 @@ public void onConfigurationChanged(Configuration newConfig) { mDrawerToggle.onConfigurationChanged(newConfig); } - @Override - public boolean onCreateOptionsMenu(Menu menu) { - return super.onCreateOptionsMenu(menu); - } - @Override public boolean onPrepareOptionsMenu(Menu menu) { boolean showItems = !mDrawerLayout.isDrawerOpen(Gravity.START) && mSurveyGroup != null; - for (int i=0; i activityWeakReference; + + private SurveySyncBroadcastReceiver(SurveyActivity activity) { + this.activityWeakReference = new WeakReference<>(activity); + } + @Override public void onReceive(Context context, Intent intent) { Log.i(TAG, "Surveys have been synchronised. Refreshing data..."); - mDrawer.load(); + SurveyActivity surveyActivity = activityWeakReference.get(); + if (surveyActivity != null) { + surveyActivity.reloadDrawer(); + } } - }; + } + + private void reloadDrawer() { + mDrawer.load(); + } } diff --git a/app/src/main/java/org/akvo/flow/activity/TimeCheckActivity.java b/app/src/main/java/org/akvo/flow/activity/TimeCheckActivity.java index ca1f479cc..e32516be4 100644 --- a/app/src/main/java/org/akvo/flow/activity/TimeCheckActivity.java +++ b/app/src/main/java/org/akvo/flow/activity/TimeCheckActivity.java @@ -24,7 +24,6 @@ import android.widget.TextView; import org.akvo.flow.R; -import org.akvo.flow.service.ApkUpdateService; import org.akvo.flow.service.DataSyncService; import org.akvo.flow.service.SurveyDownloadService; import org.akvo.flow.service.TimeCheckService; @@ -59,7 +58,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { // to time changes (the ones interacting with S3) startService(new Intent(this, SurveyDownloadService.class)); startService(new Intent(this, DataSyncService.class)); - startService(new Intent(this, ApkUpdateService.class)); startService(new Intent(this, TimeCheckService.class));// Re-check time setting status finish(); } diff --git a/app/src/main/java/org/akvo/flow/api/FlowApi.java b/app/src/main/java/org/akvo/flow/api/FlowApi.java index 3a0327569..3e6ac9a98 100644 --- a/app/src/main/java/org/akvo/flow/api/FlowApi.java +++ b/app/src/main/java/org/akvo/flow/api/FlowApi.java @@ -16,67 +16,210 @@ package org.akvo.flow.api; -import java.io.IOException; -import java.io.UnsupportedEncodingException; -import java.net.URLEncoder; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.List; -import java.util.TimeZone; - -import javax.crypto.Mac; -import javax.crypto.spec.SecretKeySpec; - -import android.annotation.SuppressLint; import android.content.Context; +import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.text.TextUtils; import android.util.Base64; import android.util.Log; -import org.akvo.flow.serialization.response.SurveyedLocaleParser; -import org.akvo.flow.domain.response.SurveyedLocalesResponse; +import org.akvo.flow.BuildConfig; import org.akvo.flow.app.FlowApp; +import org.akvo.flow.domain.Survey; import org.akvo.flow.domain.SurveyedLocale; +import org.akvo.flow.domain.response.SurveyedLocalesResponse; import org.akvo.flow.exception.HttpException; import org.akvo.flow.exception.HttpException.Status; +import org.akvo.flow.serialization.form.SurveyMetaParser; +import org.akvo.flow.serialization.response.SurveyedLocaleParser; import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.HttpUtil; import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.PropertyUtil; import org.akvo.flow.util.StatusUtil; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.HttpURLConnection; +import java.net.URLEncoder; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.text.SimpleDateFormat; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + +import javax.crypto.Mac; +import javax.crypto.spec.SecretKeySpec; public class FlowApi { + private static final String TAG = FlowApi.class.getSimpleName(); - - private static final String BASE_URL; + + //These values never change private static final String API_KEY; private static final String PHONE_NUMBER; private static final String IMEI; + private static final String ANDROID_ID; + + private static final int ERROR_UNKNOWN = -1; + private static final String HMAC_SHA_1_ALGORITHM = "HmacSHA1"; + private static final String CHARSET_UTF8 = "UTF-8"; + + private static final String HTTPS_PREFIX = "https"; + private static final String HTTP_PREFIX = "http"; static { Context context = FlowApp.getApp(); - BASE_URL = StatusUtil.getServerBase(context); API_KEY = getApiKey(context); PHONE_NUMBER = StatusUtil.getPhoneNumber(context); IMEI = StatusUtil.getImei(context); + ANDROID_ID = PlatformUtil.getAndroidID(context); + } + + public String getServerTime(@NonNull String serverBase) throws IOException { + if (serverBase.startsWith(HTTPS_PREFIX)) { + serverBase = HTTP_PREFIX + serverBase.substring(HTTPS_PREFIX.length()); + } + final String url = buildServerTimeUrl(serverBase); + String response = HttpUtil.httpGet(url); + String time = ""; + if (!TextUtils.isEmpty(response)) { + JSONObject json; + try { + json = new JSONObject(response); + time = json.getString("time"); + } catch (JSONException e1) { + Log.e(TAG, "Error fetching time: ", e1); + } + } + return time; + } + + @NonNull + private String buildServerTimeUrl(@NonNull String serverBase) { + Uri.Builder builder = Uri.parse(serverBase).buildUpon(); + builder.appendPath(Path.TIME_CHECK); + builder.appendQueryParameter(Param.TIMESTAMP, System.currentTimeMillis() + ""); + return builder.build().toString(); + } + + /** + * Request the notifications GAE has ready for us, like the list of missing files. + * + * @return String body of the HTTP response + * @throws Exception + */ + @Nullable + public JSONObject getDeviceNotification(@NonNull String serverBase, @NonNull String[] surveyIds) + throws Exception { + // Send the list of surveys we've got downloaded, getting notified of the deleted ones + String url = buildDeviceNotificationUrl(serverBase, surveyIds); + String response = HttpUtil.httpGet(url); + if (!TextUtils.isEmpty(response)) { + return new JSONObject(response); + } + return null; + } + + @NonNull + private String buildDeviceNotificationUrl(@NonNull String serverBase, + @NonNull String[] surveyIds) { + Uri.Builder builder = Uri.parse(serverBase).buildUpon(); + builder.appendPath(Path.DEVICE_NOTIFICATION); + appendDeviceParams(builder); + for (String id : surveyIds) { + builder.appendQueryParameter(Param.FORM_ID, id); + } + return builder.build().toString(); } - - public List getSurveyedLocales(long surveyGroup, String timestamp) + + @NonNull + public List getSurveyHeader(@NonNull String serverBaseUrl, @NonNull String surveyId) + throws IOException { + final String url = buildSurveyHeaderUrl(serverBaseUrl, surveyId); + String response = HttpUtil.httpGet(url); + if (response != null) { + return new SurveyMetaParser().parseList(response, true); + } + return Collections.emptyList(); + } + + @NonNull + private String buildSurveyHeaderUrl(@NonNull String serverBaseUrl, @NonNull String surveyId) { + Uri.Builder builder = Uri.parse(serverBaseUrl).buildUpon(); + builder.appendPath(Path.SURVEY_HEADER_SERVICE); + builder.appendQueryParameter(Param.PARAM_ACTION, Param.VALUE_HEADER); + builder.appendQueryParameter(Param.SURVEY_ID, surveyId); + appendDeviceParams(builder); + return builder.build().toString(); + } + + public List getSurveys(@NonNull String serverBase) throws IOException { + List surveys = new ArrayList<>(); + final String url = buildSurveysUrl(serverBase); + String response = HttpUtil.httpGet(url); + if (response != null) { + surveys = new SurveyMetaParser().parseList(response); + } + return surveys; + } + + @NonNull + private String buildSurveysUrl(@NonNull String serverBaseUrl) { + Uri.Builder builder = Uri.parse(serverBaseUrl).buildUpon(); + builder.appendPath(Path.SURVEY_LIST_SERVICE); + builder.appendQueryParameter(Param.PARAM_ACTION, Param.VALUE_SURVEY); + appendDeviceParams(builder); + return builder.build().toString(); + } + + /** + * Notify GAE back-end that data is available + * Sends a message to the service with the file name that was just uploaded + * so it can start processing the file + */ + public int sendProcessingNotification(@NonNull String serverBaseUrl, @NonNull String formId, + @NonNull String + action, @NonNull String fileName) { + String url = buildProcessingNotificationUrl(serverBaseUrl, formId, action, fileName); + try { + HttpUtil.httpGet(url); + return HttpURLConnection.HTTP_OK; + } catch (HttpException e) { + Log.e(TAG, e.getStatus() + " response for formId: " + formId); + return e.getStatus(); + } catch (Exception e) { + Log.e(TAG, "GAE sync notification failed for file: " + fileName); + return ERROR_UNKNOWN; + } + } + + @NonNull + private String buildProcessingNotificationUrl(@NonNull String serverBaseUrl, + @NonNull String formId, @NonNull + String action, @NonNull String fileName) { + Uri.Builder builder = Uri.parse(serverBaseUrl).buildUpon(); + builder.appendPath(Path.NOTIFICATION); + builder.appendQueryParameter(Param.PARAM_ACTION, action); + builder.appendQueryParameter(Param.FORM_ID, formId); + builder.appendQueryParameter(Param.FILENAME, fileName); + appendDeviceParams(builder); + return builder.build().toString(); + } + + @Nullable + public List getSurveyedLocales(@NonNull String serverBaseUrl, long surveyGroup, + @NonNull String timestamp) throws IOException { - Context context = FlowApp.getApp(); // Note: To compute the HMAC auth token, query params must be alphabetically ordered - final String query = Param.ANDROID_ID + URLEncode(PlatformUtil.getAndroidID(context)) - + "&" + Param.IMEI + URLEncode(IMEI) - + "&" + Param.LAST_UPDATED + (!TextUtils.isEmpty(timestamp)? timestamp : "0") - + "&" + Param.PHONE_NUMBER + URLEncode(PHONE_NUMBER) - + "&" + Param.SURVEY_GROUP + surveyGroup - + "&" + Param.TIMESTAMP + getTimestamp(); - - final String url = BASE_URL + Path.SURVEYED_LOCALE - + "?" + query - + "&" + Param.HMAC + getAuthorization(query); + String url = buildSyncUrl(serverBaseUrl, surveyGroup, timestamp); String response = HttpUtil.httpGet(url); if (response != null) { SurveyedLocalesResponse slRes = new SurveyedLocaleParser().parseResponse(response); @@ -85,80 +228,121 @@ public List getSurveyedLocales(long surveyGroup, String timestam } return slRes.getSurveyedLocales(); } - + return null; } - private static String URLEncode(String param) { + @NonNull + private String buildSyncUrl(@NonNull String serverBaseUrl, long surveyGroup, + @NonNull String timestamp) { + // Note: To compute the HMAC auth token, query params must be alphabetically ordered + StringBuilder queryStringBuilder = new StringBuilder(); + appendParam(queryStringBuilder, Param.ANDROID_ID, encodeParam(ANDROID_ID)); + appendParam(queryStringBuilder, Param.IMEI, encodeParam(IMEI)); + appendParam(queryStringBuilder, Param.LAST_UPDATED, (!TextUtils.isEmpty(timestamp) ? + timestamp : "0")); + appendParam(queryStringBuilder, Param.PHONE_NUMBER, encodeParam(PHONE_NUMBER)); + appendParam(queryStringBuilder, Param.SURVEY_GROUP, surveyGroup + ""); + queryStringBuilder.append(Param.TIMESTAMP).append(Param.EQUALS).append(getTimestamp()); + final String query = queryStringBuilder.toString(); + return serverBaseUrl + "/" + Path.SURVEYED_LOCALE + "?" + query + + Param.SEPARATOR + Param.HMAC + Param.EQUALS + getAuthorization(query); + } + + private void appendParam(@NonNull StringBuilder queryStringBuilder, @NonNull String paramName, + @NonNull String paramValue) { + queryStringBuilder.append(paramName).append(Param.EQUALS).append(paramValue).append(Param + .SEPARATOR); + } + + private String encodeParam(@Nullable String param) { if (TextUtils.isEmpty(param)) { return ""; } try { - return URLEncoder.encode(param, "UTF-8"); + return URLEncoder.encode(param, CHARSET_UTF8); } catch (UnsupportedEncodingException e) { Log.e(TAG, e.getMessage()); return ""; } } - private static String getApiKey(Context context) { + private static String getApiKey(@NonNull Context context) { PropertyUtil props = new PropertyUtil(context.getResources()); return props.getProperty(ConstantUtil.API_KEY); } - - private String getAuthorization(String query) { + + @Nullable + private String getAuthorization(@NonNull String query) { String authorization = null; try { - SecretKeySpec signingKey = new SecretKeySpec(API_KEY.getBytes(), "HmacSHA1"); + SecretKeySpec signingKey = new SecretKeySpec(API_KEY.getBytes(), HMAC_SHA_1_ALGORITHM); - Mac mac = Mac.getInstance("HmacSHA1"); + Mac mac = Mac.getInstance(HMAC_SHA_1_ALGORITHM); mac.init(signingKey); byte[] rawHmac = mac.doFinal(query.getBytes()); authorization = Base64.encodeToString(rawHmac, Base64.DEFAULT); - } catch (NoSuchAlgorithmException | InvalidKeyException e) { + } catch (@NonNull NoSuchAlgorithmException | InvalidKeyException e) { Log.e(TAG, e.getMessage()); } - + return authorization; } - - @SuppressLint("SimpleDateFormat") + private String getTimestamp() { - SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss"); + SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss", Locale.US); dateFormat.setTimeZone(TimeZone.getTimeZone("GMT")); - + try { - return URLEncoder.encode(dateFormat.format(new Date()), "UTF-8"); + return URLEncoder.encode(dateFormat.format(new Date()), CHARSET_UTF8); } catch (UnsupportedEncodingException e) { Log.e(TAG, e.getMessage()); return null; } } - public static String getDeviceParams() { + private void appendDeviceParams(@NonNull Uri.Builder builder) { Context context = FlowApp.getApp(); - return Param.PHONE_NUMBER + URLEncode(PHONE_NUMBER) - + "&" + Param.ANDROID_ID + URLEncode(PlatformUtil.getAndroidID(context)) - + "&" + Param.IMEI + URLEncode(IMEI) - + "&" + Param.VERSION + URLEncode(PlatformUtil.getVersionName(context)) - + "&" + Param.DEVICE_ID + URLEncode(StatusUtil.getDeviceId(context)); + builder.appendQueryParameter(Param.PHONE_NUMBER, PHONE_NUMBER); + builder.appendQueryParameter(Param.ANDROID_ID, ANDROID_ID); + builder.appendQueryParameter(Param.IMEI, IMEI); + builder.appendQueryParameter(Param.VERSION, BuildConfig.VERSION_NAME); + builder.appendQueryParameter(Param.DEVICE_ID, StatusUtil.getDeviceId(context)); } - + interface Path { - String SURVEYED_LOCALE = "/surveyedlocale"; + + String SURVEYED_LOCALE = "surveyedlocale"; + String NOTIFICATION = "processor"; + String SURVEY_LIST_SERVICE = "surveymanager"; + String SURVEY_HEADER_SERVICE = "surveymanager"; + String DEVICE_NOTIFICATION = "devicenotification"; + String TIME_CHECK = "devicetimerest"; } - + interface Param { - String SURVEY_GROUP = "surveyGroupId="; - String PHONE_NUMBER = "phoneNumber="; - String IMEI = "imei="; - String TIMESTAMP = "ts="; - String LAST_UPDATED = "lastUpdateTime="; - String HMAC = "h="; - String VERSION = "ver="; - String DEVICE_ID = "devId="; - String ANDROID_ID = "androidId="; + + String SURVEY_GROUP = "surveyGroupId"; + String PHONE_NUMBER = "phoneNumber"; + String IMEI = "imei"; + String TIMESTAMP = "ts"; + String LAST_UPDATED = "lastUpdateTime"; + String HMAC = "h"; + String VERSION = "ver"; + String DEVICE_ID = "devId"; + String ANDROID_ID = "androidId"; + + String PARAM_ACTION = "action"; + String FORM_ID = "formID"; + String SURVEY_ID = "surveyId"; + String FILENAME = "fileName"; + + String VALUE_HEADER = "getSurveyHeader"; + String VALUE_SURVEY = "getAvailableSurveysDevice"; + + String SEPARATOR = "&"; + String EQUALS = "="; } } diff --git a/app/src/main/java/org/akvo/flow/api/S3Api.java b/app/src/main/java/org/akvo/flow/api/S3Api.java index c58dff5bd..3f7633c8f 100644 --- a/app/src/main/java/org/akvo/flow/api/S3Api.java +++ b/app/src/main/java/org/akvo/flow/api/S3Api.java @@ -9,7 +9,6 @@ import org.akvo.flow.util.FileUtil; import org.akvo.flow.util.HttpUtil; import org.akvo.flow.util.PropertyUtil; -import org.apache.http.HttpStatus; import java.io.BufferedInputStream; import java.io.BufferedOutputStream; @@ -113,7 +112,7 @@ public void get(String objectKey, File dst) throws IOException { HttpUtil.copyStream(in, out); int status = conn.getResponseCode(); - if (status != HttpStatus.SC_OK) { + if (status != HttpURLConnection.HTTP_OK) { throw new IOException("Status Code: " + status + ". Expected: 200 - OK"); } } finally { diff --git a/app/src/main/java/org/akvo/flow/app/FlowApp.java b/app/src/main/java/org/akvo/flow/app/FlowApp.java index 51772f948..1d2ac4393 100644 --- a/app/src/main/java/org/akvo/flow/app/FlowApp.java +++ b/app/src/main/java/org/akvo/flow/app/FlowApp.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2013-2017 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -30,6 +30,7 @@ import org.akvo.flow.domain.Survey; import org.akvo.flow.domain.SurveyGroup; import org.akvo.flow.domain.User; +import org.akvo.flow.service.ApkUpdateService; import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.LangsPreferenceUtil; import org.akvo.flow.util.Prefs; @@ -44,14 +45,21 @@ public class FlowApp extends Application { private Locale mLocale; private User mUser; private long mSurveyGroupId;// Hacky way of filtering the survey group in Record search + private Prefs prefs; @Override public void onCreate() { super.onCreate(); + prefs = new Prefs(getApplicationContext()); init(); + startUpdateService(); app = this; } + private void startUpdateService() { + ApkUpdateService.scheduleFirstTask(this); + } + public static FlowApp getApp() { return app; } @@ -90,14 +98,14 @@ private void init() { loadLastUser(); // Load last survey group - mSurveyGroupId = Prefs.getLong(this, Prefs.KEY_SURVEY_GROUP_ID, SurveyGroup.ID_NONE); + mSurveyGroupId = prefs.getLong(Prefs.KEY_SURVEY_GROUP_ID, SurveyGroup.ID_NONE); mSurveyChecker.run();// Ensure surveys have put their languages } public void setUser(User user) { mUser = user; - Prefs.setLong(this, Prefs.KEY_USER_ID, mUser != null ? mUser.getId() : -1); + prefs.setLong(Prefs.KEY_USER_ID, mUser != null ? mUser.getId() : -1); } public User getUser() { @@ -106,7 +114,7 @@ public User getUser() { public void setSurveyGroupId(long surveyGroupId) { mSurveyGroupId = surveyGroupId; - Prefs.setLong(this, Prefs.KEY_SURVEY_GROUP_ID, surveyGroupId); + prefs.setLong(Prefs.KEY_SURVEY_GROUP_ID, surveyGroupId); } public long getSurveyGroupId() { @@ -137,11 +145,11 @@ private void loadLastUser() { database.open(); // Consider the app set up if the DB contains users. This is relevant for v2.2.0 app upgrades - if (!Prefs.getBoolean(this, Prefs.KEY_SETUP, false)) { - Prefs.setBoolean(this, Prefs.KEY_SETUP, database.getUsers().getCount() > 0); + if (!prefs.getBoolean(Prefs.KEY_SETUP, false)) { + prefs.setBoolean(Prefs.KEY_SETUP, database.getUsers().getCount() > 0); } - long id = Prefs.getLong(this, Prefs.KEY_USER_ID, -1); + long id = prefs.getLong(Prefs.KEY_USER_ID, -1); if (id != -1) { Cursor cur = database.getUser(id); if (cur.moveToFirst()) { diff --git a/app/src/main/java/org/akvo/flow/dao/DataProvider.java b/app/src/main/java/org/akvo/flow/dao/DataProvider.java index 690892ea3..06abc5968 100644 --- a/app/src/main/java/org/akvo/flow/dao/DataProvider.java +++ b/app/src/main/java/org/akvo/flow/dao/DataProvider.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + * + */ + package org.akvo.flow.dao; import android.app.SearchManager; @@ -7,6 +24,8 @@ import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.net.Uri; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import org.akvo.flow.app.FlowApp; import org.akvo.flow.dao.SurveyDbAdapter.DatabaseHelper; @@ -50,8 +69,10 @@ public Cursor query(Uri uri, String[] projection, String selection, String[] sel // Suggestions search // Adjust incoming query to become SQL text match long surveyGroupId = FlowApp.getApp().getSurveyGroupId(); - - final String term = selectionArgs[0] + "%"; + + String nameSearchTerm = createNameSearchTerm(selectionArgs); + String idSearchTerm = createIdSearchTerm(selectionArgs); + projection = new String[] { RecordColumns._ID, RecordColumns.RECORD_ID @@ -64,7 +85,7 @@ public Cursor query(Uri uri, String[] projection, String selection, String[] sel Tables.RECORD, projection, SUGGEST_SELECTION, - new String[]{String.valueOf(surveyGroupId), term, term}, + new String[]{String.valueOf(surveyGroupId), idSearchTerm, nameSearchTerm}, null, null, sortOrder); break; } @@ -77,6 +98,27 @@ public Cursor query(Uri uri, String[] projection, String selection, String[] sel return cursor; } + @NonNull + private String createIdSearchTerm(@Nullable String[] selectionArgs) { + StringBuilder recordIdSearchBuilder = new StringBuilder(); + if (selectionArgs != null && selectionArgs.length > 0) { + recordIdSearchBuilder.append(selectionArgs[0]); + } + recordIdSearchBuilder.append("%"); + return recordIdSearchBuilder.toString(); + } + + @NonNull + private String createNameSearchTerm(@Nullable String[] selectionArgs) { + StringBuilder nameSearchBuilder = new StringBuilder(); + nameSearchBuilder.append("%"); + if (selectionArgs != null && selectionArgs.length > 0) { + nameSearchBuilder.append(selectionArgs[0]); + } + nameSearchBuilder.append("%"); + return nameSearchBuilder.toString(); + } + @Override public int delete(Uri uri, String selection, String[] selectionArgs) { // No types yet (only suggestion, which is managed (semi)automatically) diff --git a/app/src/main/java/org/akvo/flow/dao/SurveyDbAdapter.java b/app/src/main/java/org/akvo/flow/dao/SurveyDbAdapter.java index 4366a2c36..04c928e3e 100644 --- a/app/src/main/java/org/akvo/flow/dao/SurveyDbAdapter.java +++ b/app/src/main/java/org/akvo/flow/dao/SurveyDbAdapter.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -16,16 +16,6 @@ package org.akvo.flow.dao; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Date; -import java.util.HashMap; -import java.util.HashSet; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; - import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -46,15 +36,27 @@ import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.PlatformUtil; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Date; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Iterator; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + /** * Database class for the survey db. It can create/upgrade the database as well * as select/insert/update survey responses. TODO: break this up into separate * DAOs - * + * * @author Christopher Fagiani */ public class SurveyDbAdapter { + public static final int DOES_NOT_EXIST = -1; + public interface Tables { String SURVEY = "survey"; String SURVEY_INSTANCE = "survey_instance"; @@ -79,27 +81,27 @@ public interface Tables { } public interface SurveyGroupColumns { - String _ID = "_id"; - String SURVEY_GROUP_ID = "survey_group_id"; - String NAME = "name"; + String _ID = "_id"; + String SURVEY_GROUP_ID = "survey_group_id"; + String NAME = "name"; String REGISTER_SURVEY_ID = "register_survey_id"; - String MONITORED = "monitored"; + String MONITORED = "monitored"; } - + public interface RecordColumns { - String _ID = "_id"; - String RECORD_ID = "record_id"; - String SURVEY_GROUP_ID = "survey_group_id"; - String NAME = "name"; - String LATITUDE = "latitude"; - String LONGITUDE = "longitude"; - String LAST_MODIFIED = "last_modified"; - } - + String _ID = "_id"; + String RECORD_ID = "record_id"; + String SURVEY_GROUP_ID = "survey_group_id"; + String NAME = "name"; + String LATITUDE = "latitude"; + String LONGITUDE = "longitude"; + String LAST_MODIFIED = "last_modified"; + } + public interface SyncTimeColumns { - String _ID = "_id"; - String SURVEY_GROUP_ID = "survey_group_id"; - String TIME = "time"; + String _ID = "_id"; + String SURVEY_GROUP_ID = "survey_group_id"; + String TIME = "time"; } /** @@ -118,7 +120,10 @@ public interface SurveyInstanceColumns { String SUBMITTED_DATE = "submitted_date"; String EXPORTED_DATE = "exported_date"; String SYNC_DATE = "sync_date"; - String STATUS = "status";// Denormalized value. See 'SurveyInstanceStatus' + /** + * Denormalized value. see {@link SurveyInstanceStatus} + **/ + String STATUS = "status"; String DURATION = "duration"; String SUBMITTER = "submitter";// Submitter name. Added in DB version 79 String VERSION = "version"; @@ -171,18 +176,18 @@ public interface PreferencesColumns { } public interface SurveyInstanceStatus { - int SAVED = 0; - int SUBMITTED = 1; - int EXPORTED = 2; - int SYNCED = 3; + int SAVED = 0; + int SUBMITTED = 1; + int EXPORTED = 2; + int SYNCED = 3; int DOWNLOADED = 4; } public interface TransmissionStatus { - int QUEUED = 0; - int IN_PROGRESS = 1; - int SYNCED = 2; - int FAILED = 3; + int QUEUED = 0; + int IN_PROGRESS = 1; + int SYNCED = 2; + int FAILED = 3; int FORM_DELETED = 4; } @@ -199,7 +204,6 @@ public interface TransmissionStatus { "INSERT INTO preferences VALUES('user.storelast','false')", "INSERT INTO preferences VALUES('data.cellular.upload','true')", "INSERT INTO preferences VALUES('user.lastuser.id','')", - "INSERT INTO preferences VALUES('location.sendbeacon','false')", "INSERT INTO preferences VALUES('backend.server','')", "INSERT INTO preferences VALUES('screen.keepon','true')", "INSERT INTO preferences VALUES('survey.textsize','LARGE')", @@ -221,7 +225,7 @@ public interface TransmissionStatus { /** * Helper class for creating the database tables and loading reference data * It is declared with package scope for VM optimizations - * + * * @author Christopher Fagiani */ static class DatabaseHelper extends SQLiteOpenHelper { @@ -328,9 +332,9 @@ public void onCreate(SQLiteDatabase db) { @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { Log.d(TAG, "Upgrading database from version " + oldVersion + " to " + newVersion); - + int version = oldVersion; - + // Apply database updates sequentially. It starts in the current // version, hooking into the correspondent case block, and falls // through to any future upgrade. If no break statement is found, @@ -353,7 +357,7 @@ public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (version != DATABASE_VERSION) { Log.d(TAG, "onUpgrade() - Recreating the Database."); - + db.execSQL("DROP TABLE IF EXISTS " + Tables.RESPONSE); db.execSQL("DROP TABLE IF EXISTS " + Tables.SYNC_TIME); db.execSQL("DROP TABLE IF EXISTS " + Tables.SURVEY); @@ -398,17 +402,18 @@ public void close() { } } } - + private void createIndexes(SQLiteDatabase db) { // Included in point updates db.execSQL("CREATE INDEX response_idx ON " + Tables.RESPONSE + "(" - + ResponseColumns.SURVEY_INSTANCE_ID + ", " + ResponseColumns.QUESTION_ID + ")"); + + ResponseColumns.SURVEY_INSTANCE_ID + ", " + ResponseColumns.QUESTION_ID + + ")"); db.execSQL("CREATE INDEX record_name_idx ON " + Tables.RECORD - + "(" + RecordColumns.NAME +")"); + + "(" + RecordColumns.NAME + ")"); db.execSQL("CREATE INDEX response_status_idx ON " + Tables.SURVEY_INSTANCE - + "(" + SurveyInstanceColumns.STATUS +")"); + + "(" + SurveyInstanceColumns.STATUS + ")"); db.execSQL("CREATE INDEX response_modified_idx ON " + Tables.SURVEY_INSTANCE - + "(" + SurveyInstanceColumns.SUBMITTED_DATE +")"); + + "(" + SurveyInstanceColumns.SUBMITTED_DATE + ")"); } /** @@ -418,16 +423,17 @@ public String findPreference(SQLiteDatabase db, String key) { String value = null; Cursor cursor = db.query(Tables.PREFERENCES, new String[] { - PreferencesColumns.KEY, - PreferencesColumns.VALUE + PreferencesColumns.KEY, + PreferencesColumns.VALUE }, PreferencesColumns.KEY + " = ?", new String[] { - key + key }, null, null, null); if (cursor != null) { if (cursor.getCount() > 0) { cursor.moveToFirst(); - value = cursor.getString(cursor.getColumnIndexOrThrow(PreferencesColumns.VALUE)); + value = cursor + .getString(cursor.getColumnIndexOrThrow(PreferencesColumns.VALUE)); } cursor.close(); } @@ -441,9 +447,9 @@ public void savePreference(SQLiteDatabase db, String key, String value) { ContentValues updatedValues = new ContentValues(); updatedValues.put(PreferencesColumns.VALUE, value); int updated = db.update(Tables.PREFERENCES, updatedValues, PreferencesColumns.KEY - + " = ?", + + " = ?", new String[] { - key + key }); if (updated <= 0) { updatedValues.put(PreferencesColumns.KEY, key); @@ -455,7 +461,7 @@ public void savePreference(SQLiteDatabase db, String key, String value) { /** * Constructor - takes the context to allow the database to be * opened/created - * + * * @param ctx the Context within which to work */ public SurveyDbAdapter(Context ctx) { @@ -464,7 +470,7 @@ public SurveyDbAdapter(Context ctx) { /** * Open or create the db - * + * * @throws SQLException if the database could be neither opened or created */ public SurveyDbAdapter open() throws SQLException { @@ -507,7 +513,7 @@ public Cursor getResponsesData(long surveyInstanceId) { * updates the status of a survey instance to the status passed in. * Status must be one of the 'SurveyInstanceStatus' one. The corresponding * Date column will be updated with the current timestamp. - * + * * @param surveyInstanceId * @param status */ @@ -556,7 +562,7 @@ public void updateSurveyStatus(long surveyInstanceId, int status) { public void addSurveyDuration(long respondentId, long sessionDuration) { final String sql = "UPDATE " + Tables.SURVEY_INSTANCE + " SET " + SurveyInstanceColumns.DURATION + " = " - + SurveyInstanceColumns.DURATION + " + " + sessionDuration + + SurveyInstanceColumns.DURATION + " + " + sessionDuration + " WHERE " + SurveyInstanceColumns._ID + " = " + respondentId + " AND " + SurveyInstanceColumns.SUBMITTED_DATE + " IS NULL"; database.execSQL(sql); @@ -564,7 +570,7 @@ public void addSurveyDuration(long respondentId, long sessionDuration) { /** * returns a cursor listing all users - * + * * @return */ public Cursor getUsers() { @@ -581,7 +587,7 @@ public Cursor getUsers() { /** * retrieves a user by ID - * + * * @param id * @return */ @@ -611,7 +617,7 @@ public long createOrUpdateUser(Long id, String name) { idVal = database.insert(Tables.USER, null, initialValues); } else { database.update(Tables.USER, initialValues, UserColumns._ID + "=?", - new String[]{idVal.toString()}); + new String[] { idVal.toString() }); } return idVal; } @@ -659,7 +665,7 @@ public Map getResponses(long surveyInstanceId) { /** * loads a single question response - * + * * @param surveyInstanceId * @param questionId * @return @@ -668,12 +674,12 @@ public QuestionResponse getResponse(Long surveyInstanceId, String questionId) { QuestionResponse resp = null; Cursor cursor = database.query(Tables.RESPONSE, new String[] { - ResponseColumns._ID, ResponseColumns.QUESTION_ID, ResponseColumns.ANSWER, - ResponseColumns.TYPE, ResponseColumns.SURVEY_INSTANCE_ID, - ResponseColumns.INCLUDE, ResponseColumns.FILENAME + ResponseColumns._ID, ResponseColumns.QUESTION_ID, ResponseColumns.ANSWER, + ResponseColumns.TYPE, ResponseColumns.SURVEY_INSTANCE_ID, + ResponseColumns.INCLUDE, ResponseColumns.FILENAME }, ResponseColumns.SURVEY_INSTANCE_ID + " = ? AND " + ResponseColumns.QUESTION_ID - + " =?", + + " =?", new String[] { String.valueOf(surveyInstanceId), questionId }, null, null, null); if (cursor != null && cursor.moveToFirst()) { @@ -683,9 +689,11 @@ public QuestionResponse getResponse(Long surveyInstanceId, String questionId) { resp.setType(cursor.getString(cursor.getColumnIndexOrThrow(ResponseColumns.TYPE))); resp.setValue(cursor.getString(cursor.getColumnIndexOrThrow(ResponseColumns.ANSWER))); resp.setId(cursor.getLong(cursor.getColumnIndexOrThrow(ResponseColumns._ID))); - resp.setFilename(cursor.getString(cursor.getColumnIndexOrThrow(ResponseColumns.FILENAME))); + resp.setFilename( + cursor.getString(cursor.getColumnIndexOrThrow(ResponseColumns.FILENAME))); - boolean include = cursor.getInt(cursor.getColumnIndexOrThrow(ResponseColumns.INCLUDE)) == 1; + boolean include = + cursor.getInt(cursor.getColumnIndexOrThrow(ResponseColumns.INCLUDE)) == 1; resp.setIncludeFlag(include); } @@ -699,7 +707,7 @@ public QuestionResponse getResponse(Long surveyInstanceId, String questionId) { /** * inserts or updates a question response after first looking to see if it * already exists in the database. - * + * * @param resp * @return */ @@ -722,14 +730,14 @@ public QuestionResponse createOrUpdateSurveyResponse(QuestionResponse resp) { initialValues.put(ResponseColumns.QUESTION_ID, responseToSave.getQuestionId()); initialValues.put(ResponseColumns.SURVEY_INSTANCE_ID, responseToSave.getRespondentId()); initialValues.put(ResponseColumns.FILENAME, responseToSave.getFilename()); - initialValues.put(ResponseColumns.INCLUDE, resp.getIncludeFlag() ? 1: 0); + initialValues.put(ResponseColumns.INCLUDE, resp.getIncludeFlag() ? 1 : 0); if (responseToSave.getId() == null) { id = database.insert(Tables.RESPONSE, null, initialValues); } else { if (database.update(Tables.RESPONSE, initialValues, ResponseColumns._ID + "=?", new String[] { responseToSave.getId().toString() - }) > 0) { + }) > 0) { id = responseToSave.getId(); } } @@ -741,7 +749,8 @@ public QuestionResponse createOrUpdateSurveyResponse(QuestionResponse resp) { /** * creates a new unsubmitted survey instance */ - public long createSurveyRespondent(String surveyId, double version, User user, String surveyedLocaleId) { + public long createSurveyRespondent(String surveyId, double version, User user, + String surveyedLocaleId) { final long time = System.currentTimeMillis(); ContentValues initialValues = new ContentValues(); @@ -762,7 +771,7 @@ public long createSurveyRespondent(String surveyId, double version, User user, S * returns a list of survey objects that are out of date (missing from the * db or with a lower version number). If a survey is present but marked as * deleted, it will not be listed as out of date (and thus won't be updated) - * + * * @param surveys * @return */ @@ -771,7 +780,7 @@ public List checkSurveyVersions(List surveys) { for (int i = 0; i < surveys.size(); i++) { Cursor cursor = database.query(Tables.SURVEY, new String[] { - SurveyColumns.SURVEY_ID + SurveyColumns.SURVEY_ID }, SurveyColumns.SURVEY_ID + " = ? and (" + SurveyColumns.VERSION + " >= ? or " + SurveyColumns.DELETED + " = ?)", new String[] { @@ -799,7 +808,7 @@ public void markSurveyHelpDownloaded(String surveyId, boolean isDownloaded) { if (database.update(Tables.SURVEY, updatedValues, SurveyColumns.SURVEY_ID + " = ?", new String[] { - surveyId + surveyId }) < 1) { Log.e(TAG, "Could not update record for Survey " + surveyId); } @@ -807,22 +816,22 @@ public void markSurveyHelpDownloaded(String surveyId, boolean isDownloaded) { /** * updates a survey in the db and resets the deleted flag to "N" - * + * * @param survey * @return */ public void saveSurvey(Survey survey) { Cursor cursor = database.query(Tables.SURVEY, new String[] { - SurveyColumns._ID + SurveyColumns._ID }, SurveyColumns.SURVEY_ID + " = ?", new String[] { - survey.getId(), + survey.getId(), }, null, null, null); - final long surveyGroupId = survey.getSurveyGroup() != null ? - survey.getSurveyGroup().getId() + final long surveyGroupId = survey.getSurveyGroup() != null ? + survey.getSurveyGroup().getId() : SurveyGroup.ID_NONE; - + ContentValues updatedValues = new ContentValues(); updatedValues.put(SurveyColumns.SURVEY_ID, survey.getId()); updatedValues.put(SurveyColumns.VERSION, survey.getVersion()); @@ -840,7 +849,7 @@ public void saveSurvey(Survey survey) { // if we found an item, it's an update, otherwise, it's an insert database.update(Tables.SURVEY, updatedValues, SurveyColumns.SURVEY_ID + " = ?", new String[] { - survey.getId() + survey.getId() }); } else { database.insert(Tables.SURVEY, null, updatedValues); @@ -857,12 +866,12 @@ public void saveSurvey(Survey survey) { public Survey getSurvey(String surveyId) { Survey survey = null; Cursor cursor = database.query(Tables.SURVEY, new String[] { - SurveyColumns.SURVEY_ID, SurveyColumns.NAME, SurveyColumns.LOCATION, - SurveyColumns.FILENAME, SurveyColumns.TYPE, SurveyColumns.LANGUAGE, - SurveyColumns.HELP_DOWNLOADED, SurveyColumns.VERSION - }, SurveyColumns.SURVEY_ID + " = ?", + SurveyColumns.SURVEY_ID, SurveyColumns.NAME, SurveyColumns.LOCATION, + SurveyColumns.FILENAME, SurveyColumns.TYPE, SurveyColumns.LANGUAGE, + SurveyColumns.HELP_DOWNLOADED, SurveyColumns.VERSION + }, SurveyColumns.SURVEY_ID + " = ?", new String[] { - surveyId + surveyId }, null, null, null); if (cursor != null) { if (cursor.moveToFirst()) { @@ -884,7 +893,7 @@ public String getPreference(String key) { /** * Lists all settings from the database */ - public HashMap getPreferences () { + public HashMap getPreferences() { HashMap settings = new HashMap(); Cursor cursor = database.query(Tables.PREFERENCES, new String[] { PreferencesColumns.KEY, PreferencesColumns.VALUE @@ -924,32 +933,32 @@ public void deleteAllSurveys() { public void deleteResponses(String surveyInstanceId) { database.delete(Tables.RESPONSE, ResponseColumns.SURVEY_INSTANCE_ID + "= ?", new String[] { - surveyInstanceId + surveyInstanceId }); } /** * deletes the surveyInstanceId record and any responses it contains - * + * * @param surveyInstanceId */ public void deleteSurveyInstance(String surveyInstanceId) { deleteResponses(surveyInstanceId); database.delete(Tables.SURVEY_INSTANCE, SurveyInstanceColumns._ID + "=?", - new String[]{ + new String[] { surveyInstanceId }); } /** * deletes a single response - * + * * @param surveyInstanceId * @param questionId */ public void deleteResponse(long surveyInstanceId, String questionId) { database.delete(Tables.RESPONSE, ResponseColumns.SURVEY_INSTANCE_ID + "= ? AND " - + ResponseColumns.QUESTION_ID + "= ?", new String[]{ + + ResponseColumns.QUESTION_ID + "= ?", new String[] { String.valueOf(surveyInstanceId), questionId }); @@ -959,8 +968,8 @@ public void createTransmission(long surveyInstanceId, String formID, String file createTransmission(surveyInstanceId, formID, filename, TransmissionStatus.QUEUED); } - - public void createTransmission(long surveyInstanceId, String formID, String filename, int status) { + public void createTransmission(long surveyInstanceId, String formID, String filename, + int status) { ContentValues values = new ContentValues(); values.put(TransmissionColumns.SURVEY_INSTANCE_ID, surveyInstanceId); values.put(TransmissionColumns.SURVEY_ID, formID); @@ -995,7 +1004,7 @@ public int updateTransmissionHistory(String fileName, int status) { return database.update(Tables.TRANSMISSION, vals, TransmissionColumns.FILENAME + " = ?", - new String[] {fileName}); + new String[] { fileName }); } public List getFileTransmissions(Cursor cursor) { @@ -1007,7 +1016,8 @@ public List getFileTransmissions(Cursor cursor) { final int endCol = cursor.getColumnIndexOrThrow(TransmissionColumns.END_DATE); final int idCol = cursor.getColumnIndexOrThrow(TransmissionColumns._ID); final int formIdCol = cursor.getColumnIndexOrThrow(TransmissionColumns.SURVEY_ID); - final int surveyInstanceCol = cursor.getColumnIndexOrThrow(TransmissionColumns.SURVEY_INSTANCE_ID); + final int surveyInstanceCol = cursor + .getColumnIndexOrThrow(TransmissionColumns.SURVEY_INSTANCE_ID); final int fileCol = cursor.getColumnIndexOrThrow(TransmissionColumns.FILENAME); final int statusCol = cursor.getColumnIndexOrThrow(TransmissionColumns.STATUS); @@ -1076,7 +1086,7 @@ public List getUnsyncedTransmissions() { /** * executes a single insert/update/delete DML or any DDL statement without * any bind arguments. - * + * * @param sql */ public void executeSql(String sql) { @@ -1088,7 +1098,8 @@ public void executeSql(String sql) { * The survey xml must exist in the APK */ public void reinstallTestSurvey() { - executeSql("insert into survey values(999991,'Sample Survey', 1.0,'Survey','res','testsurvey','english','N','N')"); + executeSql( + "insert into survey values(999991,'Sample Survey', 1.0,'Survey','res','testsurvey','english','N','N')"); } /** @@ -1120,14 +1131,14 @@ public void clearCollectedData() { /** * performs a soft-delete on a user - * + * * @param id */ public void deleteUser(Long id) { ContentValues updatedValues = new ContentValues(); updatedValues.put(UserColumns.DELETED, 1); database.update(Tables.USER, updatedValues, UserColumns._ID + " = ?", - new String[]{ + new String[] { id.toString() }); } @@ -1195,7 +1206,7 @@ public void addLanguages(String[] values) { savePreference(ConstantUtil.SURVEY_LANG_PRESENT_KEY, newLangsPresentIndexes); } - + public void addSurveyGroup(SurveyGroup surveyGroup) { ContentValues values = new ContentValues(); values.put(SurveyGroupColumns.SURVEY_GROUP_ID, surveyGroup.getId()); @@ -1204,24 +1215,27 @@ public void addSurveyGroup(SurveyGroup surveyGroup) { values.put(SurveyGroupColumns.MONITORED, surveyGroup.isMonitored() ? 1 : 0); database.insert(Tables.SURVEY_GROUP, null, values); } - + public static SurveyGroup getSurveyGroup(Cursor cursor) { long id = cursor.getLong(cursor.getColumnIndexOrThrow(SurveyGroupColumns.SURVEY_GROUP_ID)); String name = cursor.getString(cursor.getColumnIndexOrThrow(SurveyGroupColumns.NAME)); - String registerSurveyId = cursor.getString(cursor.getColumnIndexOrThrow(SurveyGroupColumns.REGISTER_SURVEY_ID)); - boolean monitored = cursor.getInt(cursor.getColumnIndexOrThrow(SurveyGroupColumns.MONITORED)) > 0; + String registerSurveyId = cursor + .getString(cursor.getColumnIndexOrThrow(SurveyGroupColumns.REGISTER_SURVEY_ID)); + boolean monitored = + cursor.getInt(cursor.getColumnIndexOrThrow(SurveyGroupColumns.MONITORED)) > 0; return new SurveyGroup(id, name, registerSurveyId, monitored); } public SurveyGroup getSurveyGroup(long id) { SurveyGroup sg = null; Cursor c = database.query(Tables.SURVEY_GROUP, - new String[]{ - SurveyGroupColumns._ID, SurveyGroupColumns.SURVEY_GROUP_ID, SurveyGroupColumns.NAME, + new String[] { + SurveyGroupColumns._ID, SurveyGroupColumns.SURVEY_GROUP_ID, + SurveyGroupColumns.NAME, SurveyGroupColumns.REGISTER_SURVEY_ID, SurveyGroupColumns.MONITORED }, SurveyGroupColumns.SURVEY_GROUP_ID + "= ?", - new String[] {String.valueOf(id)}, + new String[] { String.valueOf(id) }, null, null, null); if (c != null && c.moveToFirst()) { sg = getSurveyGroup(c); @@ -1234,22 +1248,23 @@ public SurveyGroup getSurveyGroup(long id) { public Cursor getSurveyGroups() { return database.query(Tables.SURVEY_GROUP, new String[] { - SurveyGroupColumns._ID, SurveyGroupColumns.SURVEY_GROUP_ID, SurveyGroupColumns.NAME, + SurveyGroupColumns._ID, SurveyGroupColumns.SURVEY_GROUP_ID, + SurveyGroupColumns.NAME, SurveyGroupColumns.REGISTER_SURVEY_ID, SurveyGroupColumns.MONITORED }, null, null, null, null, SurveyGroupColumns.NAME); } - + public String createSurveyedLocale(long surveyGroupId) { String id = PlatformUtil.recordUuid(); ContentValues values = new ContentValues(); values.put(RecordColumns.RECORD_ID, id); values.put(RecordColumns.SURVEY_GROUP_ID, surveyGroupId); database.insert(Tables.RECORD, null, values); - + return id; } - + public static SurveyedLocale getSurveyedLocale(Cursor cursor) { String id = cursor.getString(RecordQuery.RECORD_ID); long surveyGroupId = cursor.getLong(RecordQuery.SURVEY_GROUP_ID); @@ -1269,25 +1284,25 @@ public static SurveyedLocale getSurveyedLocale(Cursor cursor) { public Cursor getSurveyedLocales(long surveyGroupId) { return database.query(Tables.RECORD, RecordQuery.PROJECTION, RecordColumns.SURVEY_GROUP_ID + " = ?", - new String[] {String.valueOf(surveyGroupId)}, + new String[] { String.valueOf(surveyGroupId) }, null, null, null); } - + public SurveyedLocale getSurveyedLocale(String surveyedLocaleId) { Cursor cursor = database.query(Tables.RECORD, RecordQuery.PROJECTION, RecordColumns.RECORD_ID + " = ?", - new String[] {String.valueOf(surveyedLocaleId)}, + new String[] { String.valueOf(surveyedLocaleId) }, null, null, null); - + SurveyedLocale locale = null; if (cursor.moveToFirst()) { locale = getSurveyedLocale(cursor); } cursor.close(); - + return locale; } - + public static Survey getSurvey(Cursor cursor) { Survey survey = new Survey(); survey.setId(cursor.getString(cursor.getColumnIndexOrThrow(SurveyColumns.SURVEY_ID))); @@ -1298,7 +1313,8 @@ public static Survey getSurvey(Cursor cursor) { survey.setLanguage(cursor.getString(cursor.getColumnIndexOrThrow(SurveyColumns.LANGUAGE))); survey.setVersion(cursor.getDouble(cursor.getColumnIndexOrThrow(SurveyColumns.VERSION))); - int helpDownloaded = cursor.getInt(cursor.getColumnIndexOrThrow(SurveyColumns.HELP_DOWNLOADED)); + int helpDownloaded = cursor + .getInt(cursor.getColumnIndexOrThrow(SurveyColumns.HELP_DOWNLOADED)); survey.setHelpDownloaded(helpDownloaded == 1); return survey; } @@ -1309,7 +1325,8 @@ public String[] getSurveyIds() { String[] ids = new String[c.getCount()]; if (c.moveToFirst()) { do { - ids[c.getPosition()] = c.getString(c.getColumnIndexOrThrow(SurveyColumns.SURVEY_ID)); + ids[c.getPosition()] = c + .getString(c.getColumnIndexOrThrow(SurveyColumns.SURVEY_ID)); } while (c.moveToNext()); } c.close(); @@ -1346,11 +1363,11 @@ public Cursor getSurveys(long surveyGroupId) { String.valueOf(surveyGroupId) }; } - + return database.query(Tables.SURVEY, new String[] { - SurveyColumns._ID, SurveyColumns.SURVEY_ID, SurveyColumns.NAME, - SurveyColumns.FILENAME, SurveyColumns.TYPE, SurveyColumns.LANGUAGE, - SurveyColumns.HELP_DOWNLOADED, SurveyColumns.VERSION, SurveyColumns.LOCATION + SurveyColumns._ID, SurveyColumns.SURVEY_ID, SurveyColumns.NAME, + SurveyColumns.FILENAME, SurveyColumns.TYPE, SurveyColumns.LANGUAGE, + SurveyColumns.HELP_DOWNLOADED, SurveyColumns.VERSION, SurveyColumns.LOCATION }, whereClause, whereParams, null, null, null); } @@ -1360,7 +1377,7 @@ public Cursor getSurveys(long surveyGroupId) { * parsing the Cursor columns. * To get the Cursor result, use getSurveys(surveyGroupId) */ - public List getSurveyList (long surveyGroupId) { + public List getSurveyList(long surveyGroupId) { // Reuse getSurveys() method Cursor cursor = getSurveys(surveyGroupId); @@ -1396,7 +1413,7 @@ public void deleteSurvey(String surveyId) { ContentValues updatedValues = new ContentValues(); updatedValues.put(SurveyColumns.DELETED, 1); database.update(Tables.SURVEY, updatedValues, SurveyColumns.SURVEY_ID + " = ?", - new String[]{surveyId}); + new String[] { surveyId }); } public Cursor getFormInstance(long formInstanceId) { @@ -1428,7 +1445,7 @@ public Cursor getFormInstances(String recordId) { public long[] getFormInstances(String recordId, String surveyId, int status) { String where = Tables.SURVEY_INSTANCE + "." + SurveyInstanceColumns.SURVEY_ID + "= ?" + " AND " + SurveyInstanceColumns.STATUS + "= ?" + - " AND " + SurveyInstanceColumns.RECORD_ID + "= ?"; + " AND " + SurveyInstanceColumns.RECORD_ID + "= ?"; List args = new ArrayList(); args.add(surveyId); args.add(String.valueOf(status)); @@ -1455,6 +1472,7 @@ public long[] getFormInstances(String recordId, String surveyId, int status) { /** * Given a particular surveyedLocale and one of its surveys, * retrieves the ID of the last surveyInstance matching that criteria + * * @param surveyedLocaleId * @param surveyId * @return last surveyInstance with those attributes @@ -1462,30 +1480,30 @@ public long[] getFormInstances(String recordId, String surveyId, int status) { public Long getLastSurveyInstance(String surveyedLocaleId, String surveyId) { Cursor cursor = database.query(Tables.SURVEY_INSTANCE, new String[] { - SurveyInstanceColumns._ID, SurveyInstanceColumns.RECORD_ID, - SurveyInstanceColumns.SURVEY_ID, SurveyInstanceColumns.SUBMITTED_DATE + SurveyInstanceColumns._ID, SurveyInstanceColumns.RECORD_ID, + SurveyInstanceColumns.SURVEY_ID, SurveyInstanceColumns.SUBMITTED_DATE }, SurveyInstanceColumns.RECORD_ID + "= ? AND " + SurveyInstanceColumns.SURVEY_ID + "= ? AND " + SurveyInstanceColumns.SUBMITTED_DATE + " IS NOT NULL", - new String[]{surveyedLocaleId, surveyId}, + new String[] { surveyedLocaleId, surveyId }, null, null, SurveyInstanceColumns.SUBMITTED_DATE + " DESC"); if (cursor != null && cursor.moveToFirst()) { return cursor.getLong(cursor.getColumnIndexOrThrow(SurveyInstanceColumns._ID)); } - + return null; } - + public String getSurveyedLocaleId(long surveyInstanceId) { Cursor cursor = database.query(Tables.SURVEY_INSTANCE, new String[] { - SurveyInstanceColumns._ID, SurveyInstanceColumns.RECORD_ID + SurveyInstanceColumns._ID, SurveyInstanceColumns.RECORD_ID }, SurveyInstanceColumns._ID + "= ?", - new String[]{String.valueOf(surveyInstanceId)}, + new String[] { String.valueOf(surveyInstanceId) }, null, null, null); - + String id = null; if (cursor.moveToFirst()) { id = cursor.getString(cursor.getColumnIndexOrThrow(SurveyInstanceColumns.RECORD_ID)); @@ -1493,13 +1511,16 @@ public String getSurveyedLocaleId(long surveyInstanceId) { cursor.close(); return id; } - + /** * Flag to indicate the type of locale update from a given response */ - public enum SurveyedLocaleMeta {NAME, GEOLOCATION} - - public void updateSurveyedLocale(long surveyInstanceId, String response, SurveyedLocaleMeta type) { + public enum SurveyedLocaleMeta { + NAME, GEOLOCATION + } + + public void updateSurveyedLocale(long surveyInstanceId, String response, + SurveyedLocaleMeta type) { if (!TextUtils.isEmpty(response)) { String surveyedLocaleId = getSurveyedLocaleId(surveyInstanceId); ContentValues surveyedLocaleValues = new ContentValues(); @@ -1508,7 +1529,7 @@ public void updateSurveyedLocale(long surveyInstanceId, String response, Surveye metaResponse.setRespondentId(surveyInstanceId); metaResponse.setValue(response); metaResponse.setIncludeFlag(true); - + switch (type) { case NAME: surveyedLocaleValues.put(RecordColumns.NAME, response); @@ -1526,12 +1547,12 @@ public void updateSurveyedLocale(long surveyInstanceId, String response, Surveye metaResponse.setQuestionId(ConstantUtil.QUESTION_LOCALE_GEO); break; } - + // Update the surveyed locale info database.update(Tables.RECORD, surveyedLocaleValues, RecordColumns.RECORD_ID + " = ?", - new String[] {surveyedLocaleId}); - + new String[] { surveyedLocaleId }); + // Store the META_NAME/META_GEO as a response createOrUpdateSurveyResponse(metaResponse); } @@ -1545,13 +1566,14 @@ public void updateRecordModifiedDate(String recordId, long timestamp) { values.put(RecordColumns.LAST_MODIFIED, timestamp); database.update(Tables.RECORD, values, RecordColumns.RECORD_ID + " = ? AND " + RecordColumns.LAST_MODIFIED + " < ?", - new String[]{recordId, String.valueOf(timestamp)}); + new String[] { recordId, String.valueOf(timestamp) }); } - + /** - * Filters surveyd locales based on the parameters passed in. - */ - public Cursor getFilteredSurveyedLocales(long surveyGroupId, Double latitude, Double longitude, int orderBy) { + * Filters surveyd locales based on the parameters passed in. + */ + public Cursor getFilteredSurveyedLocales(long surveyGroupId, Double latitude, Double longitude, + int orderBy) { // Note: This PROJECTION column indexes have to match the default RecordQuery PROJECTION ones, // as this one will only APPEND new columns to the resultset, making the generic getSurveyedLocale(Cursor) // fully compatible. TODO: This should be refactored and replaced with a less complex approach. @@ -1569,15 +1591,19 @@ public Cursor getFilteredSurveyedLocales(long surveyGroupId, Double latitude, Do orderByStr = " ORDER BY " + RecordColumns.LAST_MODIFIED + " DESC";// By date break; case ConstantUtil.ORDER_BY_DISTANCE: - if (latitude != null && longitude != null){ + if (latitude != null && longitude != null) { // this is to correct the distance for the shortening at higher latitudes - Double fudge = Math.pow(Math.cos(Math.toRadians(latitude)),2); + Double fudge = Math.pow(Math.cos(Math.toRadians(latitude)), 2); // this uses a simple planar approximation of distance. this should be good enough for our purpose. - String orderByTempl = " ORDER BY CASE WHEN " + RecordColumns.LATITUDE + " IS NULL THEN 1 ELSE 0 END," - + " ((%s - " + RecordColumns.LATITUDE + ") * (%s - " + RecordColumns.LATITUDE - + ") + (%s - " + RecordColumns.LONGITUDE + ") * (%s - " + RecordColumns.LONGITUDE + ") * %s)"; - orderByStr = String.format(orderByTempl, latitude, latitude, longitude, longitude, fudge); + String orderByTempl = " ORDER BY CASE WHEN " + RecordColumns.LATITUDE + + " IS NULL THEN 1 ELSE 0 END," + + " ((%s - " + RecordColumns.LATITUDE + ") * (%s - " + + RecordColumns.LATITUDE + + ") + (%s - " + RecordColumns.LONGITUDE + ") * (%s - " + + RecordColumns.LONGITUDE + ") * %s)"; + orderByStr = String + .format(orderByTempl, latitude, latitude, longitude, longitude, fudge); } break; case ConstantUtil.ORDER_BY_STATUS: @@ -1588,58 +1614,61 @@ public Cursor getFilteredSurveyedLocales(long surveyGroupId, Double latitude, Do break; } - String[] whereValues = new String[] {String.valueOf(surveyGroupId)}; + String[] whereValues = new String[] { String.valueOf(surveyGroupId) }; return database.rawQuery(queryString + whereClause + groupBy + orderByStr, whereValues); } // ======================================================= // // =========== SurveyedLocales synchronization =========== // // ======================================================= // - + public void syncResponses(List responses, long surveyInstanceId) { for (QuestionResponse response : responses) { Cursor cursor = database.query(Tables.RESPONSE, - new String[] { ResponseColumns.SURVEY_INSTANCE_ID, ResponseColumns.QUESTION_ID }, + new String[] { ResponseColumns.SURVEY_INSTANCE_ID, ResponseColumns.QUESTION_ID + }, ResponseColumns.SURVEY_INSTANCE_ID + " = ? AND " - + ResponseColumns.QUESTION_ID + " = ?", - new String[] { String.valueOf(surveyInstanceId), response.getQuestionId()}, + + ResponseColumns.QUESTION_ID + " = ?", + new String[] { String.valueOf(surveyInstanceId), response.getQuestionId() }, null, null, null); - + boolean exists = cursor.getCount() > 0; cursor.close(); - + ContentValues values = new ContentValues(); values.put(ResponseColumns.ANSWER, response.getValue()); values.put(ResponseColumns.TYPE, response.getType()); values.put(ResponseColumns.QUESTION_ID, response.getQuestionId()); values.put(ResponseColumns.INCLUDE, response.getIncludeFlag()); values.put(ResponseColumns.SURVEY_INSTANCE_ID, surveyInstanceId); - + if (exists) { database.update(Tables.RESPONSE, values, ResponseColumns.SURVEY_INSTANCE_ID + " = ? AND " + ResponseColumns.QUESTION_ID + " = ?", - new String[] { String.valueOf(surveyInstanceId), response.getQuestionId()}); + new String[] { String.valueOf(surveyInstanceId), response.getQuestionId() + }); } else { database.insert(Tables.RESPONSE, null, values); } } } - + public void syncSurveyInstances(List surveyInstances, String surveyedLocaleId) { for (SurveyInstance surveyInstance : surveyInstances) { Cursor cursor = database.query(Tables.SURVEY_INSTANCE, new String[] { - SurveyInstanceColumns._ID, SurveyInstanceColumns.UUID}, + SurveyInstanceColumns._ID, SurveyInstanceColumns.UUID + }, SurveyInstanceColumns.UUID + " = ?", - new String[] { surveyInstance.getUuid()}, + new String[] { surveyInstance.getUuid() }, null, null, null); - - long id = -1; + + long id = DOES_NOT_EXIST; if (cursor.moveToFirst()) { id = cursor.getLong(0); } cursor.close(); - + ContentValues values = new ContentValues(); values.put(SurveyInstanceColumns.SURVEY_ID, surveyInstance.getSurveyId()); values.put(SurveyInstanceColumns.SUBMITTED_DATE, surveyInstance.getDate()); @@ -1648,9 +1677,9 @@ public void syncSurveyInstances(List surveyInstances, String sur values.put(SurveyInstanceColumns.SYNC_DATE, System.currentTimeMillis()); values.put(SurveyInstanceColumns.SUBMITTER, surveyInstance.getSubmitter()); - if (id != -1) { + if (id != DOES_NOT_EXIST) { database.update(Tables.SURVEY_INSTANCE, values, SurveyInstanceColumns.UUID - + " = ?", new String[] { surveyInstance.getUuid()}); + + " = ?", new String[] { surveyInstance.getUuid() }); } else { values.put(SurveyInstanceColumns.UUID, surveyInstance.getUuid()); id = database.insert(Tables.SURVEY_INSTANCE, null, values); @@ -1661,10 +1690,11 @@ public void syncSurveyInstances(List surveyInstances, String sur // The filename is a unique column in the transmission table, and as we do not have // a file to hold this data, we set the value to the instance UUID - createTransmission(id, surveyInstance.getSurveyId(), surveyInstance.getUuid(), TransmissionStatus.SYNCED); + createTransmission(id, surveyInstance.getSurveyId(), surveyInstance.getUuid(), + TransmissionStatus.SYNCED); } } - + public void syncSurveyedLocale(SurveyedLocale surveyedLocale) { final String id = surveyedLocale.getId(); try { @@ -1691,19 +1721,20 @@ public void syncSurveyedLocale(SurveyedLocale surveyedLocale) { database.endTransaction(); } } - + /** * Get the synchronization time for a particular survey group. + * * @param surveyGroupId id of the SurveyGroup * @return time if exists for this key, null otherwise */ public String getSyncTime(long surveyGroupId) { - Cursor cursor = database.query(Tables.SYNC_TIME, - new String[] {SyncTimeColumns.SURVEY_GROUP_ID, SyncTimeColumns.TIME}, + Cursor cursor = database.query(Tables.SYNC_TIME, + new String[] { SyncTimeColumns.SURVEY_GROUP_ID, SyncTimeColumns.TIME }, SyncTimeColumns.SURVEY_GROUP_ID + "=?", - new String[] {String.valueOf(surveyGroupId)}, + new String[] { String.valueOf(surveyGroupId) }, null, null, null); - + String time = null; if (cursor.moveToFirst()) { time = cursor.getString(cursor.getColumnIndexOrThrow(SyncTimeColumns.TIME)); @@ -1711,11 +1742,12 @@ public String getSyncTime(long surveyGroupId) { cursor.close(); return time; } - + /** * Save the time of synchronization time for a particular SurveyGroup + * * @param surveyGroupId id of the SurveyGroup - * @param time String containing the timestamp + * @param time String containing the timestamp */ public void setSyncTime(long surveyGroupId, String time) { ContentValues values = new ContentValues(); @@ -1729,7 +1761,7 @@ public void setSyncTime(long surveyGroupId, String time) { */ public void deleteEmptySurveyInstances() { executeSql("DELETE FROM " + Tables.SURVEY_INSTANCE - + " WHERE " + SurveyInstanceColumns._ID + " NOT IN " + + " WHERE " + SurveyInstanceColumns._ID + " NOT IN " + "(SELECT DISTINCT " + ResponseColumns.SURVEY_INSTANCE_ID + " FROM " + Tables.RESPONSE + ")"); } @@ -1739,7 +1771,7 @@ public void deleteEmptySurveyInstances() { */ public void deleteEmptyRecords() { executeSql("DELETE FROM " + Tables.RECORD - + " WHERE " + RecordColumns.RECORD_ID + " NOT IN " + + " WHERE " + RecordColumns.RECORD_ID + " NOT IN " + "(SELECT DISTINCT " + SurveyInstanceColumns.RECORD_ID + " FROM " + Tables.SURVEY_INSTANCE + ")"); } @@ -1748,7 +1780,7 @@ public void deleteEmptyRecords() { * Query the given table, returning a Cursor over the result set. */ public Cursor query(String table, String[] columns, String selection, String[] selectionArgs, - String groupBy, String having, String orderBy) { + String groupBy, String having, String orderBy) { return database.query(table, columns, selection, selectionArgs, groupBy, having, orderBy); } diff --git a/app/src/main/java/org/akvo/flow/domain/SurveyGroup.java b/app/src/main/java/org/akvo/flow/domain/SurveyGroup.java index 57e595c7d..2c2d135a1 100644 --- a/app/src/main/java/org/akvo/flow/domain/SurveyGroup.java +++ b/app/src/main/java/org/akvo/flow/domain/SurveyGroup.java @@ -20,18 +20,18 @@ public class SurveyGroup implements Serializable { /** - * + * */ private static final long serialVersionUID = -5146372662599353969L; public static final long ID_NONE = -1; - + private long mId; private String mName; private boolean mMonitored; private String mRegisterSurveyId; - public SurveyGroup (long id, String name, String registerSurveyId, boolean monitored) { + public SurveyGroup(long id, String name, String registerSurveyId, boolean monitored) { mId = id; mName = name; mRegisterSurveyId = registerSurveyId; @@ -45,21 +45,22 @@ public void setRegisterSurveyId(String surveyId) { public String getRegisterSurveyId() { return mRegisterSurveyId; } - + public long getId() { return mId; } - + public String getName() { return mName; } - + public boolean isMonitored() { return mMonitored; } - + @Override public String toString() { return mName; } + } diff --git a/app/src/main/java/org/akvo/flow/domain/SurveyedLocale.java b/app/src/main/java/org/akvo/flow/domain/SurveyedLocale.java index e7e687655..a0edaf4e7 100644 --- a/app/src/main/java/org/akvo/flow/domain/SurveyedLocale.java +++ b/app/src/main/java/org/akvo/flow/domain/SurveyedLocale.java @@ -1,17 +1,16 @@ /* - * Copyright (C) 2013-2014 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.domain; @@ -30,11 +29,9 @@ import java.util.List; public class SurveyedLocale implements Serializable, ClusterItem { - /** - * - */ + private static final long serialVersionUID = -3556354410813212814L; - + private String mId; private String mName; private long mLastModified; @@ -69,27 +66,27 @@ public long getSurveyGroupId() { public long getLastModified() { return mLastModified; } - + public String getId() { return mId; } - + public Double getLatitude() { return mLatitude; } - + public Double getLongitude() { return mLongitude; } - + public void setSurveyInstances(List surveyInstances) { mSurveyInstances = surveyInstances; } - + public List getSurveyInstances() { return mSurveyInstances; } - + public String getName() { return mName; } @@ -111,5 +108,4 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE public String getDisplayName(Context context) { return TextUtils.isEmpty(mName) ? context.getString(R.string.unknown) : mName; } - } diff --git a/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateMapper.java b/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateMapper.java index bce1b01de..e17744dc4 100644 --- a/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateMapper.java +++ b/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateMapper.java @@ -7,13 +7,13 @@ public class ApkUpdateMapper { @Nullable - public ApkData transform(@Nullable JSONObject json) throws JSONException { + public ViewApkData transform(@Nullable JSONObject json) throws JSONException { if (json == null) { return null; } String latestVersion = json.getString("version"); String apkUrl = json.getString("fileName"); String md5Checksum = json.optString("md5Checksum", null); - return new ApkData(latestVersion, apkUrl, md5Checksum); + return new ViewApkData(latestVersion, apkUrl, md5Checksum); } } diff --git a/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateStore.java b/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateStore.java new file mode 100644 index 000000000..e6e0a06b4 --- /dev/null +++ b/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkUpdateStore.java @@ -0,0 +1,79 @@ +/* + * Copyright (C) 2010-2017 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + * + */ + +package org.akvo.flow.domain.apkupdate; + +import android.support.annotation.Nullable; + +import org.akvo.flow.util.ConstantUtil; +import org.akvo.flow.util.PlatformUtil; +import org.akvo.flow.util.Prefs; + +//TODO: this should be moved to data package +public class ApkUpdateStore { + + public static final String KEY_APK_DATA = "apk_data"; + public static final String KEY_APP_UPDATE_LAST_NOTIFIED = "update_notified_last_time"; + public static final long NOT_NOTIFIED = -1; + + private final GsonMapper gsonMapper; + private final Prefs preferences; + + public ApkUpdateStore(GsonMapper gsonMapper, Prefs prefs) { + this.gsonMapper = gsonMapper; + this.preferences = prefs; + } + + public void updateApkData(ViewApkData apkData) { + ViewApkData savedApkData = getApkData(); + if (savedApkData == null || PlatformUtil + .isNewerVersion(savedApkData.getVersion(), apkData.getVersion())) { + saveApkData(apkData); + clearAppUpdateNotified(); + } + } + + private void saveApkData(ViewApkData apkData) { + preferences.setString(KEY_APK_DATA, gsonMapper.write(apkData, ViewApkData.class)); + } + + private void clearAppUpdateNotified() { + preferences.removePreference(KEY_APP_UPDATE_LAST_NOTIFIED); + } + + @Nullable + public ViewApkData getApkData() { + String apkDataString = preferences.getString(KEY_APK_DATA, null); + if (apkDataString == null) { + return null; + } + return gsonMapper.read(apkDataString, ViewApkData.class); + } + + public void saveAppUpdateNotifiedTime() { + preferences.setLong(KEY_APP_UPDATE_LAST_NOTIFIED, System.currentTimeMillis()); + } + + public boolean shouldNotifyNewVersion() { + long lastNotified = preferences.getLong(KEY_APP_UPDATE_LAST_NOTIFIED, NOT_NOTIFIED); + if (lastNotified == NOT_NOTIFIED) { + return true; + } + return System.currentTimeMillis() - lastNotified + >= ConstantUtil.UPDATE_NOTIFICATION_DELAY_IN_MS; + } +} diff --git a/app/src/main/java/org/akvo/flow/domain/apkupdate/GsonMapper.java b/app/src/main/java/org/akvo/flow/domain/apkupdate/GsonMapper.java new file mode 100644 index 000000000..81b4f536d --- /dev/null +++ b/app/src/main/java/org/akvo/flow/domain/apkupdate/GsonMapper.java @@ -0,0 +1,76 @@ +/* + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + * + */ + +package org.akvo.flow.domain.apkupdate; + +import android.util.Log; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonIOException; +import com.google.gson.JsonSyntaxException; + +import java.io.InputStream; +import java.io.InputStreamReader; +import java.lang.reflect.Type; + +public class GsonMapper { + + private static final String TAG = GsonMapper.class.getSimpleName(); + + private final Gson mapper; + + public GsonMapper() { + this.mapper = new GsonBuilder().create(); + } + + public T read(final String content, final Class type) throws JsonSyntaxException { + try { + return this.mapper.fromJson(content, type); + } catch (JsonSyntaxException e) { + Log.e(TAG, "Error mapping json to class '" + type + "' with contents: '" + content + "'", e); + throw e; + } + } + + public T read(final String content, final Type type) throws JsonSyntaxException { + try { + return this.mapper.fromJson(content, type); + } catch (JsonSyntaxException e) { + Log.e(TAG, "Error mapping json to class '" + type + "' with contents: '" + content + "'", e); + throw e; + } + } + + public T read(final InputStream content, final Class type) throws JsonIOException, JsonSyntaxException { + try { + return this.mapper.fromJson(new InputStreamReader(content), type); + } catch (JsonIOException | JsonSyntaxException e) { + Log.e(TAG, "Error mapping json to class '" + type + "' with contents: '" + content + "'", e); + throw e; + } + } + + public String write(final T content, final Class type) { + try { + return this.mapper.toJson(content, type); + } catch (JsonIOException | JsonSyntaxException e) { + Log.e(TAG, "Error mapping class '" + type + "' to json with contents: '" + content + "'", e); + throw e; + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkData.java b/app/src/main/java/org/akvo/flow/domain/apkupdate/ViewApkData.java similarity index 52% rename from app/src/main/java/org/akvo/flow/domain/apkupdate/ApkData.java rename to app/src/main/java/org/akvo/flow/domain/apkupdate/ViewApkData.java index bf822c7b9..0d6918197 100644 --- a/app/src/main/java/org/akvo/flow/domain/apkupdate/ApkData.java +++ b/app/src/main/java/org/akvo/flow/domain/apkupdate/ViewApkData.java @@ -15,18 +15,51 @@ */ package org.akvo.flow.domain.apkupdate; -public class ApkData { +import android.os.Parcel; +import android.os.Parcelable; + +public class ViewApkData implements Parcelable { private final String version; private final String fileUrl; private final String md5Checksum; - public ApkData(String version, String fileUrl, String md5Checksum) { + public ViewApkData(String version, String fileUrl, String md5Checksum) { this.version = version; this.fileUrl = fileUrl; this.md5Checksum = md5Checksum; } + protected ViewApkData(Parcel in) { + version = (String) in.readValue(String.class.getClassLoader()); + fileUrl = (String) in.readValue(String.class.getClassLoader()); + md5Checksum = (String) in.readValue(String.class.getClassLoader()); + } + + @Override + public void writeToParcel(Parcel dest, int flags) { + dest.writeValue(version); + dest.writeValue(fileUrl); + dest.writeValue(md5Checksum); + } + + @Override + public int describeContents() { + return 0; + } + + public static final Creator CREATOR = new Creator() { + @Override + public ViewApkData createFromParcel(Parcel in) { + return new ViewApkData(in); + } + + @Override + public ViewApkData[] newArray(int size) { + return new ViewApkData[size]; + } + }; + public String getVersion() { return version; } diff --git a/app/src/main/java/org/akvo/flow/domain/response/SurveyedLocalesResponse.java b/app/src/main/java/org/akvo/flow/domain/response/SurveyedLocalesResponse.java index d21bb7d16..f09fe1be3 100644 --- a/app/src/main/java/org/akvo/flow/domain/response/SurveyedLocalesResponse.java +++ b/app/src/main/java/org/akvo/flow/domain/response/SurveyedLocalesResponse.java @@ -1,24 +1,23 @@ /* - * Copyright (C) 2013 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.domain.response; -import java.util.List; + import org.akvo.flow.domain.SurveyedLocale; -import org.akvo.flow.domain.SurveyedLocale; + import java.util.List; public class SurveyedLocalesResponse { private List mSurveyedLocales; @@ -28,7 +27,7 @@ public SurveyedLocalesResponse(List surveyedLocales, String erro mSurveyedLocales = surveyedLocales; mError = error; } - + public List getSurveyedLocales() { return mSurveyedLocales; } diff --git a/app/src/main/java/org/akvo/flow/exception/HttpException.java b/app/src/main/java/org/akvo/flow/exception/HttpException.java index a717904c3..9224e7ab0 100644 --- a/app/src/main/java/org/akvo/flow/exception/HttpException.java +++ b/app/src/main/java/org/akvo/flow/exception/HttpException.java @@ -16,8 +16,6 @@ package org.akvo.flow.exception; -import org.apache.http.HttpStatus; - import java.io.IOException; /** @@ -30,7 +28,7 @@ public class HttpException extends IOException { * This error codes extend the already existent HTTP status codes, in order to communicate * internal API error codes not present in the http layer. */ - public interface Status extends HttpStatus { + public interface Status { // Custom codes start on 600 (preserving any existent http status unchanged) int MALFORMED_RESPONSE = 600; } diff --git a/app/src/main/java/org/akvo/flow/serialization/form/SurveyMetaParser.java b/app/src/main/java/org/akvo/flow/serialization/form/SurveyMetaParser.java index 7ebfc3c4b..7a2c55a3f 100644 --- a/app/src/main/java/org/akvo/flow/serialization/form/SurveyMetaParser.java +++ b/app/src/main/java/org/akvo/flow/serialization/form/SurveyMetaParser.java @@ -1,35 +1,34 @@ /* - * Copyright (C) 2013 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.serialization.form; +import android.support.annotation.NonNull; import android.text.TextUtils; -import java.util.ArrayList; -import java.util.List; -import java.util.StringTokenizer; - import org.akvo.flow.domain.Survey; import org.akvo.flow.domain.SurveyGroup; import org.akvo.flow.util.ConstantUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.StringTokenizer; + /** * Parser for Survey definitions (CSV). No question-answer pairs * will be returned. - * */ public class SurveyMetaParser { @@ -43,7 +42,7 @@ public Survey parse(String response) { survey.setName(touple[Attr.NAME]); survey.setLanguage(touple[Attr.LANGUAGE]); survey.setVersion(Double.parseDouble(touple[Attr.VERSION])); - + // Parse the SurveyGroup long groupId = Long.parseLong(touple[Attr.GROUP_ID]); String groupName = touple[Attr.GROUP_NAME]; @@ -56,23 +55,24 @@ public Survey parse(String response) { } SurveyGroup group = new SurveyGroup(groupId, groupName, registerSurveyId, monitored); - + survey.setSurveyGroup(group); - + survey.setType(ConstantUtil.FILE_SURVEY_LOCATION_TYPE); return survey; } - + /** * Survey metadata feeds might contain no phone, thus we will * need to prepend the rows with a fake comma to ensure consistency. - * + * * @param response * @param addColumn * @return survey list */ + @NonNull public List parseList(String response, boolean addColumn) { - List surveyList = new ArrayList(); + List surveyList = new ArrayList<>(); StringTokenizer strTok = new StringTokenizer(response, "\n"); while (strTok.hasMoreTokens()) { String currentLine = strTok.nextToken(); @@ -85,7 +85,7 @@ public List parseList(String response, boolean addColumn) { surveyList.add(survey); } } - + return surveyList; } @@ -94,19 +94,19 @@ public List parseList(String response) { } interface Attr { - int DEVICE = 0;// Unused attribute. Should not be sent - int ID = 1; - int NAME = 2; - int LANGUAGE = 3; - int VERSION = 4; - + int DEVICE = 0;// Unused attribute. Should not be sent + int ID = 1; + int NAME = 2; + int LANGUAGE = 3; + int VERSION = 4; + // SurveyGroup information - int GROUP_ID = 5; - int GROUP_NAME = 6; - int GROUP_MONITORED = 7; + int GROUP_ID = 5; + int GROUP_NAME = 6; + int GROUP_MONITORED = 7; int GROUP_REGISTRATION_SURVEY = 8; - - int COUNT = 9;// Length of column array + + int COUNT = 9;// Length of column array } } diff --git a/app/src/main/java/org/akvo/flow/serialization/response/SurveyedLocaleParser.java b/app/src/main/java/org/akvo/flow/serialization/response/SurveyedLocaleParser.java index ecf729552..477a854d5 100644 --- a/app/src/main/java/org/akvo/flow/serialization/response/SurveyedLocaleParser.java +++ b/app/src/main/java/org/akvo/flow/serialization/response/SurveyedLocaleParser.java @@ -18,17 +18,16 @@ import android.util.Log; -import java.util.ArrayList; -import java.util.List; - +import org.akvo.flow.domain.SurveyInstance; +import org.akvo.flow.domain.SurveyedLocale; +import org.akvo.flow.domain.response.SurveyedLocalesResponse; import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; -import org.akvo.flow.domain.response.SurveyedLocalesResponse; -import org.akvo.flow.domain.SurveyInstance; -import org.akvo.flow.domain.SurveyedLocale; +import java.util.ArrayList; +import java.util.List; public class SurveyedLocaleParser { private static final String TAG = SurveyedLocaleParser.class.getSimpleName(); @@ -39,7 +38,7 @@ public SurveyedLocalesResponse parseResponse(String response) { try { JSONObject jResponse = new JSONObject(response); JSONArray jSurveyedLocales = jResponse.getJSONArray(Attrs.SURVEYED_LOCALE_DATA); - for (int i=0; i surveyInstances = new SurveyInstanceParser().parseList(jSurveyInstances); + List surveyInstances = new SurveyInstanceParser() + .parseList(jSurveyInstances); SurveyedLocale surveyedLocale = new SurveyedLocale(id, name, lastModified, surveyGroupId, latitude, longitude); @@ -76,19 +78,19 @@ public SurveyedLocale parseSurveyedLocale(JSONObject jSurveyedLocale) throws JSO return surveyedLocale; } - + interface Attrs { // Main response - String SURVEYED_LOCALE_DATA = "surveyedLocaleData"; - + String SURVEYED_LOCALE_DATA = "surveyedLocaleData"; + // SurveyedLocale - String ID = "id"; - String SURVEY_GROUP_ID = "surveyGroupId"; - String NAME = "displayName"; - String LATITUDE = "lat"; - String LONGITUDE = "lon"; + String ID = "id"; + String SURVEY_GROUP_ID = "surveyGroupId"; + String NAME = "displayName"; + String LATITUDE = "lat"; + String LONGITUDE = "lon"; String SURVEY_INSTANCES = "surveyInstances"; - String LAST_MODIFIED = "lastUpdateDateTime"; + String LAST_MODIFIED = "lastUpdateDateTime"; } } diff --git a/app/src/main/java/org/akvo/flow/service/ApkUpdateHelper.java b/app/src/main/java/org/akvo/flow/service/ApkUpdateHelper.java index 19c989a89..b85a87745 100644 --- a/app/src/main/java/org/akvo/flow/service/ApkUpdateHelper.java +++ b/app/src/main/java/org/akvo/flow/service/ApkUpdateHelper.java @@ -13,48 +13,46 @@ * * The full license text can also be seen at . */ + package org.akvo.flow.service; import android.content.Context; import android.support.annotation.NonNull; import android.support.annotation.Nullable; -import java.io.IOException; +import android.support.v4.util.Pair; + +import org.akvo.flow.BuildConfig; import org.akvo.flow.api.service.ApkApiService; -import org.akvo.flow.domain.apkupdate.ApkData; +import org.akvo.flow.domain.apkupdate.ViewApkData; import org.akvo.flow.domain.apkupdate.ApkUpdateMapper; -import org.akvo.flow.ui.Navigator; import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.StringUtil; import org.json.JSONException; import org.json.JSONObject; +import java.io.IOException; + public class ApkUpdateHelper { private final ApkApiService apkApiService = new ApkApiService(); private final ApkUpdateMapper apkUpdateMapper = new ApkUpdateMapper(); - private final Navigator navigator = new Navigator(); public ApkUpdateHelper() { } - boolean shouldUpdate(@NonNull Context context) throws IOException, JSONException { + Pair shouldUpdate(@NonNull Context context) throws IOException, JSONException { JSONObject json = apkApiService.getApkDataObject(context); - ApkData data = apkUpdateMapper.transform(json); - if (shouldAppBeUpdated(data, context)) { - // There is a newer version. Fire the 'Download and Install' Activity. - navigator.navigateToAppUpdate(context, data); - return true; - } - return false; + ViewApkData data = apkUpdateMapper.transform(json); + return new Pair<>(shouldAppBeUpdated(data), data); } - private boolean shouldAppBeUpdated(@Nullable ApkData data, @NonNull Context context) { + private boolean shouldAppBeUpdated(@Nullable ViewApkData data) { if (data == null) { return false; } String version = data.getVersion(); return StringUtil.isValid(version) - && PlatformUtil.isNewerVersion(PlatformUtil.getVersionName(context), version) - && StringUtil.isValid(data.getFileUrl()); + && PlatformUtil.isNewerVersion(BuildConfig.VERSION_NAME, version) + && StringUtil.isValid(data.getFileUrl()); } } diff --git a/app/src/main/java/org/akvo/flow/service/ApkUpdateService.java b/app/src/main/java/org/akvo/flow/service/ApkUpdateService.java index 45df1a628..35dae7426 100644 --- a/app/src/main/java/org/akvo/flow/service/ApkUpdateService.java +++ b/app/src/main/java/org/akvo/flow/service/ApkUpdateService.java @@ -1,5 +1,5 @@ /* -* Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) +* Copyright (C) 2010-2017 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -16,51 +16,111 @@ package org.akvo.flow.service; -import android.app.IntentService; -import android.content.Intent; +import android.content.Context; +import android.support.v4.util.Pair; import android.util.Log; -import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; + +import com.google.android.gms.gcm.GcmNetworkManager; +import com.google.android.gms.gcm.GcmTaskService; +import com.google.android.gms.gcm.PeriodicTask; +import com.google.android.gms.gcm.Task; +import com.google.android.gms.gcm.TaskParams; + +import org.akvo.flow.domain.apkupdate.ApkUpdateStore; +import org.akvo.flow.domain.apkupdate.GsonMapper; +import org.akvo.flow.domain.apkupdate.ViewApkData; +import org.akvo.flow.util.ConstantUtil; +import org.akvo.flow.util.Prefs; import org.akvo.flow.util.StatusUtil; /** * This background service will check the rest api for a new version of the APK. - * If found, it will display a notification, requesting permission to download and - * installAppUpdate it. After clicking the notification, the app will download and installAppUpdate - * the new APK. + * If found, it will display a {@link org.akvo.flow.activity.AppUpdateActivity}, requesting + * permission to download and installAppUpdate it. * * @author Christopher Fagiani */ -public class ApkUpdateService extends IntentService { +public class ApkUpdateService extends GcmTaskService { + /** + * Tag that is unique to this task (can be used to cancel task) + */ private static final String TAG = "APK_UPDATE_SERVICE"; private final ApkUpdateHelper apkUpdateHelper = new ApkUpdateHelper(); - public ApkUpdateService() { - super(TAG); + public static void scheduleFirstTask(Context context) { + schedulePeriodicTask(context, ConstantUtil.FIRST_REPEAT_INTERVAL_IN_SECONDS, + ConstantUtil.FIRST_FLEX_INTERVAL_IN_SECOND); + } + + private static void schedulePeriodicTask(Context context, int repeatIntervalInSeconds, + int flexIntervalInSeconds) { + try { + PeriodicTask periodic = new PeriodicTask.Builder() + .setService(ApkUpdateService.class) + //repeat every x seconds + .setPeriod(repeatIntervalInSeconds) + //specify how much earlier the task can be executed (in seconds) + .setFlex(flexIntervalInSeconds) + .setTag(TAG) + //whether the task persists after device reboot + .setPersisted(true) + //if another task with same tag is already scheduled, replace it with this task + .setUpdateCurrent(true) + //set required network state + .setRequiredNetwork(Task.NETWORK_STATE_CONNECTED) + //request that charging needs not be connected + .setRequiresCharging(false).build(); + GcmNetworkManager.getInstance(context).schedule(periodic); + } catch (Exception e) { + Log.e(TAG, "scheduleFirstTask failed", e); + } + } + + /** + * Cancels the repeated task + */ + public static void cancelRepeat(Context context) { + GcmNetworkManager.getInstance(context).cancelTask(TAG, ApkUpdateService.class); } + /** + * Called when app is updated to a new version, reinstalled etc. + * Repeating tasks have to be rescheduled + */ @Override - protected void onHandleIntent(Intent intent) { - Thread.setDefaultUncaughtExceptionHandler(PersistentUncaughtExceptionHandler.getInstance()); - checkUpdates(); + public void onInitializeTasks() { + super.onInitializeTasks(); + scheduleFirstTask(this); } /** * Check if new FLOW versions are available to installAppUpdate. If a new version is available, - * we display a notification, requesting the user to download it. + * we display {@link org.akvo.flow.activity.AppUpdateActivity}, requesting the user to download + * it. */ - private void checkUpdates() { - if (!StatusUtil.hasDataConnection(this)) { - Log.d(TAG, "No internet connection. Can't perform the requested operation"); - return; + @Override + public int onRunTask(TaskParams taskParams) { + //after the first time the task is run we reschedule to a higher interval + schedulePeriodicTask(this, ConstantUtil.REPEAT_INTERVAL_IN_SECONDS, + ConstantUtil.FLEX_INTERVAL_IN_SECONDS); + if (!StatusUtil.isConnectionAllowed(this)) { + Log.d(TAG, "No available authorised connection. Can't perform the requested operation"); + return GcmNetworkManager.RESULT_SUCCESS; } try { - apkUpdateHelper.shouldUpdate(this); + Pair booleanApkDataPair = apkUpdateHelper.shouldUpdate(this); + if (booleanApkDataPair.first) { + //save to shared preferences + ApkUpdateStore store = new ApkUpdateStore(new GsonMapper(), new Prefs(this)); + store.updateApkData(booleanApkDataPair.second); + } + return GcmNetworkManager.RESULT_SUCCESS; } catch (Exception e) { - Log.e(TAG, "Could not call apk version service", e); - PersistentUncaughtExceptionHandler.recordException(e); + Log.e(TAG, "Error with apk version service", e); + return GcmNetworkManager.RESULT_FAILURE; } } } diff --git a/app/src/main/java/org/akvo/flow/service/BootstrapService.java b/app/src/main/java/org/akvo/flow/service/BootstrapService.java index dfe20892c..c7468dbd0 100644 --- a/app/src/main/java/org/akvo/flow/service/BootstrapService.java +++ b/app/src/main/java/org/akvo/flow/service/BootstrapService.java @@ -1,18 +1,18 @@ - /* - * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) - * - * This file is part of Akvo FLOW. - * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. - * - * Akvo FLOW 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 Affero General Public License included below for more details. - * - * The full license text can also be seen at . - */ +/* + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + */ package org.akvo.flow.service; @@ -45,6 +45,7 @@ import org.akvo.flow.util.FileUtil; import org.akvo.flow.util.FileUtil.FileType; import org.akvo.flow.util.LangsPreferenceUtil; +import org.akvo.flow.util.NotificationHelper; import org.akvo.flow.util.StatusUtil; import org.akvo.flow.util.ViewUtil; @@ -99,8 +100,7 @@ private void checkAndInstall() { } String startMessage = getString(R.string.bootstrapstart); - ViewUtil.displayNotification(startMessage, startMessage, this, - ConstantUtil.NOTIFICATION_BOOTSTRAP, android.R.drawable.ic_dialog_info); + displayNotification(startMessage); databaseAdapter = new SurveyDbAdapter(this); databaseAdapter.open(); try { @@ -116,8 +116,7 @@ private void checkAndInstall() { } } String endMessage = getString(R.string.bootstrapcomplete); - ViewUtil.displayNotification(endMessage, endMessage, this, - ConstantUtil.NOTIFICATION_BOOTSTRAP, android.R.drawable.ic_dialog_info); + displayNotification(endMessage); } finally { if (databaseAdapter != null) { databaseAdapter.close(); @@ -125,12 +124,22 @@ private void checkAndInstall() { } } catch (Exception e) { String errorMessage = getString(R.string.bootstraperror); - ViewUtil.displayNotification(errorMessage, errorMessage, this, - ConstantUtil.NOTIFICATION_BOOTSTRAP, android.R.drawable.ic_dialog_alert); + displayErrorNotification(errorMessage); + Log.e(TAG, "Bootstrap error", e); } } + private void displayErrorNotification(String errorMessage) { + //FIXME: why are we repeating the message in title and text? + NotificationHelper.displayErrorNotification(errorMessage, errorMessage, this, ConstantUtil.NOTIFICATION_BOOTSTRAP); + } + + private void displayNotification(String message) { + //FIXME: why are we repeating the message in title and text? + NotificationHelper.displayNotification(message, message, this, ConstantUtil.NOTIFICATION_BOOTSTRAP); + } + /** * looks for the rollback file in the zip and, if it exists, attempts to * execute the statements contained therein diff --git a/app/src/main/java/org/akvo/flow/service/DataSyncService.java b/app/src/main/java/org/akvo/flow/service/DataSyncService.java index d41bb54fd..2e182a1c3 100644 --- a/app/src/main/java/org/akvo/flow/service/DataSyncService.java +++ b/app/src/main/java/org/akvo/flow/service/DataSyncService.java @@ -17,12 +17,11 @@ package org.akvo.flow.service; import android.app.IntentService; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; import android.content.Intent; import android.database.Cursor; -import android.support.v4.app.NotificationCompat; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; +import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import android.util.Base64; import android.util.Log; @@ -32,28 +31,24 @@ import org.akvo.flow.R; import org.akvo.flow.api.FlowApi; import org.akvo.flow.api.S3Api; -import org.akvo.flow.domain.response.FormInstance; -import org.akvo.flow.domain.response.Response; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.dao.SurveyDbAdapter.ResponseColumns; import org.akvo.flow.dao.SurveyDbAdapter.SurveyInstanceColumns; -import org.akvo.flow.dao.SurveyDbAdapter.UserColumns; -import org.akvo.flow.dao.SurveyDbAdapter.TransmissionStatus; import org.akvo.flow.dao.SurveyDbAdapter.SurveyInstanceStatus; +import org.akvo.flow.dao.SurveyDbAdapter.TransmissionStatus; +import org.akvo.flow.dao.SurveyDbAdapter.UserColumns; import org.akvo.flow.domain.FileTransmission; import org.akvo.flow.domain.Survey; -import org.akvo.flow.exception.HttpException; +import org.akvo.flow.domain.response.FormInstance; +import org.akvo.flow.domain.response.Response; import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.FileUtil; import org.akvo.flow.util.FileUtil.FileType; -import org.akvo.flow.util.HttpUtil; +import org.akvo.flow.util.NotificationHelper; import org.akvo.flow.util.PropertyUtil; import org.akvo.flow.util.StatusUtil; import org.akvo.flow.util.StringUtil; -import org.akvo.flow.util.ViewUtil; - -import org.apache.http.HttpStatus; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; @@ -61,6 +56,7 @@ import java.io.File; import java.io.FileOutputStream; import java.io.IOException; +import java.net.HttpURLConnection; import java.security.InvalidKeyException; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; @@ -77,7 +73,6 @@ import javax.crypto.spec.SecretKeySpec; /** - * * Handle survey export and sync in a background thread. The export process takes * no arguments, and will try to zip all the survey instances with a SUBMITTED status * but with no EXPORT_DATE (export hasn't happened yet). Ideally, and if the service has been @@ -86,14 +81,13 @@ * execution of the service, until the zip file finally gets exported. A possible scenario for * this is the submission of a survey when the external storage is not available, postponing the * export until it gets ready. - * * After the export of the zip files, the sync will be run, attempting to upload all the non synced * files to the datastore. * * @author Christopher Fagiani - * */ public class DataSyncService extends IntentService { + private static final String TAG = "SyncService"; private static final String DELIMITER = "\t"; private static final String SPACE = "\u0020"; // safe from source whitespace reformatting @@ -104,12 +98,6 @@ public class DataSyncService extends IntentService { private static final String SURVEY_DATA_FILE_JSON = "data.json"; private static final String SIG_FILE_NAME = ".sig"; - // Sync constants - private static final String DEVICE_NOTIFICATION_PATH = "/devicenotification"; - private static final String NOTIFICATION_PATH = "/processor?action="; - private static final String FILENAME_PARAM = "&fileName="; - private static final String FORMID_PARAM = "&formID="; - private static final String DATA_CONTENT_TYPE = "application/zip"; private static final String JPEG_CONTENT_TYPE = "image/jpeg"; private static final String PNG_CONTENT_TYPE = "image/png"; @@ -126,10 +114,9 @@ public class DataSyncService extends IntentService { */ private static final int FILE_UPLOAD_RETRIES = 2; - private static final int ERROR_UNKNOWN = -1; - private PropertyUtil mProps; private SurveyDbAdapter mDatabase; + private static final String UTF_8_CHARSET = "UTF-8"; public DataSyncService() { super(TAG); @@ -169,8 +156,7 @@ private void exportSurveys() { for (long id : getUnexportedSurveys()) { ZipFileData zipFileData = formZip(id); if (zipFileData != null) { - displayNotification(NOTIFICATION_DATA_EXPORT, getString(R.string.exportcomplete), - zipFileData.formName); + displayNotification(getString(R.string.exportcomplete), zipFileData.formName); // Create new entries in the transmission queue mDatabase.createTransmission(id, zipFileData.formId, zipFileData.filename); @@ -183,6 +169,7 @@ private void exportSurveys() { } } + @NonNull private File getSurveyInstanceFile(String uuid) { return new File(FileUtil.getFilesDir(FileType.DATA), uuid + ConstantUtil.ARCHIVE_SUFFIX); } @@ -192,10 +179,12 @@ private void checkExportedFiles() { if (cursor != null) { if (cursor.moveToFirst()) { do { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(SurveyInstanceColumns._ID)); - String uuid = cursor.getString(cursor.getColumnIndexOrThrow(SurveyInstanceColumns.UUID)); + long id = cursor + .getLong(cursor.getColumnIndexOrThrow(SurveyInstanceColumns._ID)); + String uuid = cursor + .getString(cursor.getColumnIndexOrThrow(SurveyInstanceColumns.UUID)); if (!getSurveyInstanceFile(uuid).exists()) { - Log.d(TAG, "Exported file for survey " + uuid + " not found. It's status " + + Log.d(TAG, "Exported file for survey " + uuid + " not found. It's status " + "will be set to 'submitted', and will be reprocessed"); updateSurveyStatus(id, SurveyInstanceStatus.SUBMITTED); } @@ -205,6 +194,7 @@ private void checkExportedFiles() { } } + @NonNull private long[] getUnexportedSurveys() { long[] surveyInstanceIds = new long[0];// Avoid null cases Cursor cursor = mDatabase.getSurveyInstancesByStatus(SurveyInstanceStatus.SUBMITTED); @@ -212,8 +202,8 @@ private long[] getUnexportedSurveys() { surveyInstanceIds = new long[cursor.getCount()]; if (cursor.moveToFirst()) { do { - surveyInstanceIds[cursor.getPosition()] = cursor.getLong( - cursor.getColumnIndexOrThrow(SurveyInstanceColumns._ID)); + surveyInstanceIds[cursor.getPosition()] = + cursor.getLong(cursor.getColumnIndexOrThrow(SurveyInstanceColumns._ID)); } while (cursor.moveToNext()); } cursor.close(); @@ -225,7 +215,8 @@ private ZipFileData formZip(long surveyInstanceId) { try { ZipFileData zipFileData = new ZipFileData(); // Process form instance data and collect image filenames - FormInstance formInstance = processFormInstance(surveyInstanceId, zipFileData.imagePaths); + FormInstance formInstance = processFormInstance(surveyInstanceId, + zipFileData.imagePaths); if (formInstance == null) { return null; @@ -237,7 +228,8 @@ private ZipFileData formZip(long surveyInstanceId) { zipFileData.formId = String.valueOf(formInstance.getFormId()); zipFileData.formName = mDatabase.getSurvey(zipFileData.formId).getName(); - File zipFile = getSurveyInstanceFile(zipFileData.uuid);// The filename will match the Survey Instance UUID + File zipFile = getSurveyInstanceFile( + zipFileData.uuid);// The filename will match the Survey Instance UUID // Write the data into the zip file String fileName = zipFile.getAbsolutePath();// Will normalize filename. @@ -251,8 +243,9 @@ private ZipFileData formZip(long surveyInstanceId) { String signingKeyString = mProps.getProperty(SIGNING_KEY_PROP); if (!StringUtil.isNullOrEmpty(signingKeyString)) { MessageDigest sha1Digest = MessageDigest.getInstance("SHA1"); - byte[] digest = sha1Digest.digest(zipFileData.data.getBytes("UTF-8")); - SecretKeySpec signingKey = new SecretKeySpec(signingKeyString.getBytes("UTF-8"), + byte[] digest = sha1Digest.digest(zipFileData.data.getBytes(UTF_8_CHARSET)); + SecretKeySpec signingKey = new SecretKeySpec( + signingKeyString.getBytes(UTF_8_CHARSET), SIGNING_ALGORITHM); Mac mac = Mac.getInstance(SIGNING_ALGORITHM); mac.init(signingKey); @@ -263,9 +256,10 @@ private ZipFileData formZip(long surveyInstanceId) { final String checksum = "" + checkedOutStream.getChecksum().getValue(); zos.close(); - Log.i(TAG, "Closed zip output stream for file: " + fileName + ". Checksum: " + checksum); + Log.i(TAG, + "Closed zip output stream for file: " + fileName + ". Checksum: " + checksum); return zipFileData; - } catch (IOException | NoSuchAlgorithmException | InvalidKeyException e) { + } catch (@NonNull IOException | NoSuchAlgorithmException | InvalidKeyException e) { PersistentUncaughtExceptionHandler.recordException(e); Log.e(TAG, e.getMessage()); return null; @@ -276,11 +270,11 @@ private ZipFileData formZip(long surveyInstanceId) { * Writes the contents of text to a zip entry within the Zip file behind zos * named fileName */ - private void writeTextToZip(ZipOutputStream zos, String text, - String fileName) throws IOException { + private void writeTextToZip(@NonNull ZipOutputStream zos, @NonNull String text, String fileName) + throws IOException { Log.i(TAG, "Writing zip entry"); zos.putNextEntry(new ZipEntry(fileName)); - byte[] allBytes = text.getBytes("UTF-8"); + byte[] allBytes = text.getBytes(UTF_8_CHARSET); zos.write(allBytes, 0, allBytes.length); zos.closeEntry(); Log.i(TAG, "Entry Complete"); @@ -290,7 +284,9 @@ private void writeTextToZip(ZipOutputStream zos, String text, * Iterate over the survey data returned from the database and populate the * ZipFileData information, setting the UUID, Survey ID, image paths, and String data. */ - private FormInstance processFormInstance(long surveyInstanceId, List imagePaths) throws IOException { + @NonNull + private FormInstance processFormInstance(long surveyInstanceId, + @NonNull List imagePaths) { FormInstance formInstance = new FormInstance(); List responses = new ArrayList<>(); Cursor data = mDatabase.getResponsesData(surveyInstanceId); @@ -310,7 +306,8 @@ private FormInstance processFormInstance(long surveyInstanceId, List ima int filename_col = data.getColumnIndexOrThrow(ResponseColumns.FILENAME); int disp_name_col = data.getColumnIndexOrThrow(UserColumns.NAME); int email_col = data.getColumnIndexOrThrow(UserColumns.EMAIL); - int submitted_date_col = data.getColumnIndexOrThrow(SurveyInstanceColumns.SUBMITTED_DATE); + int submitted_date_col = data + .getColumnIndexOrThrow(SurveyInstanceColumns.SUBMITTED_DATE); int uuid_col = data.getColumnIndexOrThrow(SurveyInstanceColumns.UUID); int duration_col = data.getColumnIndexOrThrow(SurveyInstanceColumns.DURATION); int localeId_col = data.getColumnIndexOrThrow(SurveyInstanceColumns.RECORD_ID); @@ -333,7 +330,8 @@ private FormInstance processFormInstance(long surveyInstanceId, List ima if (formInstance.getUUID() == null) { formInstance.setUUID(data.getString(uuid_col)); - formInstance.setFormId(data.getLong(survey_fk_col));// FormInstance uses a number for this attr + formInstance.setFormId( + data.getLong(survey_fk_col));// FormInstance uses a number for this attr formInstance.setDataPointId(data.getString(localeId_col)); formInstance.setDeviceId(deviceIdentifier); formInstance.setSubmissionDate(submitted_date); @@ -350,7 +348,8 @@ private FormInstance processFormInstance(long surveyInstanceId, List ima // Ensure backwards compatibility. Old image responses may contain filenames String type = data.getString(answer_type_col); - if (ConstantUtil.IMAGE_RESPONSE_TYPE.equals(type) || ConstantUtil.VIDEO_RESPONSE_TYPE.equals(type)) { + if (ConstantUtil.IMAGE_RESPONSE_TYPE.equals(type) + || ConstantUtil.VIDEO_RESPONSE_TYPE.equals(type)) { if (!TextUtils.isEmpty(value) && new File(value).exists()) { imagePaths.add(value); } @@ -382,7 +381,8 @@ private FormInstance processFormInstance(long surveyInstanceId, List ima // replace troublesome chars in user-provided values // replaceAll() compiles a Pattern, and so is inefficient inside a loop - private String cleanVal(String val) { + @Nullable + private String cleanVal(@Nullable String val) { if (val != null) { if (val.contains(DELIMITER)) { val = val.replace(DELIMITER, SPACE); @@ -405,7 +405,6 @@ private String cleanVal(String val) { * Sync every file (zip file, images, etc) that has a non synced state. This refers to: * - Queued transmissions * - Failed transmissions - * * Each transmission will be retried up to three times. If the transmission does * not succeed in those attempts, it will be marked as failed, and retried in the next sync. * Files are uploaded to S3 and the response's ETag is compared against a locally computed @@ -423,13 +422,12 @@ private void syncFiles() { return; } - Set syncedSurveys = new HashSet();// Successful transmissions - Set unsyncedSurveys = new HashSet();// Unsuccessful transmissions + Set syncedSurveys = new HashSet<>();// Successful transmissions + Set unsyncedSurveys = new HashSet<>();// Unsuccessful transmissions final int totalFiles = transmissions.size(); displayProgressNotification(0, totalFiles); - for (int i = 0; i < totalFiles; i++) { FileTransmission transmission = transmissions.get(i); final long surveyInstanceId = transmission.getRespondentId(); @@ -456,7 +454,8 @@ private void syncFiles() { } } - private boolean syncFile(String filename, String formId, String serverBase) { + private boolean syncFile(@NonNull String filename, @NonNull String formId, + @NonNull String serverBase) { if (TextUtils.isEmpty(filename) || filename.lastIndexOf(".") < 0) { return false; } @@ -487,19 +486,19 @@ private boolean syncFile(String filename, String formId, String serverBase) { mDatabase.updateTransmissionHistory(filename, TransmissionStatus.IN_PROGRESS); int status = TransmissionStatus.FAILED; - boolean ok = false; + boolean synced = false; if (sendFile(filename, dir, contentType, isPublic, FILE_UPLOAD_RETRIES)) { - // Notify GAE back-end that data is available - switch (sendProcessingNotification(serverBase, formId, action, getDestName(filename))) { - case HttpStatus.SC_OK: + FlowApi api = new FlowApi(); + switch (api.sendProcessingNotification(serverBase, formId, action, + getDestName(filename))) { + case HttpURLConnection.HTTP_OK: status = TransmissionStatus.SYNCED;// Mark everything completed - ok = true; + synced = true; break; - case HttpStatus.SC_NOT_FOUND: + case HttpURLConnection.HTTP_NOT_FOUND: // This form has been deleted in the dashboard, thus we cannot sync it - displayNotification(formId(formId), - getString(R.string.sync_error_title, formId), getString(R.string.sync_error_message)); + displayErrorNotification(formId); status = TransmissionStatus.FORM_DELETED; break; default:// Any error code @@ -508,10 +507,10 @@ private boolean syncFile(String filename, String formId, String serverBase) { } mDatabase.updateTransmissionHistory(filename, status); - return ok; + return synced; } - private boolean sendFile(String fileAbsolutePath, String dir, String contentType, + private boolean sendFile(@NonNull String fileAbsolutePath, String dir, String contentType, boolean isPublic, int retries) { final File file = new File(fileAbsolutePath); if (!file.exists()) { @@ -545,16 +544,17 @@ private boolean sendFile(String fileAbsolutePath, String dir, String contentType * The server will provide us with a list of missing images, * so we can accordingly update their status in the database. * This will help us fixing the Issue #55 - * * Steps: * 1- Request the list of files to the server * 2- Update the status of those files in the local database */ - private void checkDeviceNotifications(String serverBase) { + private void checkDeviceNotifications(@NonNull String serverBase) { + FlowApi flowApi = new FlowApi(); try { - String response = getDeviceNotification(serverBase); - if (!TextUtils.isEmpty(response)) { - JSONObject jResponse = new JSONObject(response); + String[] surveyIds = mDatabase.getSurveyIds(); + JSONObject jResponse = flowApi.getDeviceNotification(serverBase, surveyIds); + + if (jResponse != null) { List files = parseFiles(jResponse.optJSONArray("missingFiles")); files.addAll(parseFiles(jResponse.optJSONArray("missingUnknown"))); @@ -569,7 +569,7 @@ private void checkDeviceNotifications(String serverBase) { JSONArray jForms = jResponse.optJSONArray("deletedForms"); if (jForms != null) { - for (int i=0; i parseFiles(JSONArray jFiles) throws JSONException { - List files = new ArrayList(); + @NonNull + private List parseFiles(@Nullable JSONArray jFiles) throws JSONException { + List files = new ArrayList<>(); if (jFiles != null) { - for (int i=0; i 4.0 - builder.setProgress(total, synced, false); - - // Dummy intent. Do nothing when clicked - PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(), 0); - builder.setContentIntent(intent); - - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(ConstantUtil.NOTIFICATION_DATA_SYNC, builder.build()); + String title = getString(R.string.data_sync_title); + String text = getString(R.string.data_sync_text); + NotificationHelper.displayProgressNotification(this, synced, total, title, text, + ConstantUtil.NOTIFICATION_DATA_SYNC); } /** * Display a notification showing the final status of the sync + * * @param syncedForms number of successful transmissions * @param failedForms number of failed transmissions */ private void displaySyncedNotification(int syncedForms, int failedForms) { // Do not show failed if there is none - String text = failedForms > 0 ? String.format(getString(R.string.data_sync_all), - syncedForms, failedForms) - : String.format(getString(R.string.data_sync_synced), syncedForms); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this) - .setSmallIcon(android.R.drawable.stat_sys_upload_done) - .setContentTitle(getString(R.string.data_sync_title)) - .setContentText(text) - .setTicker(text) - .setOngoing(false); - - // Progress will only be displayed in Android versions > 4.0 - builder.setProgress(1, 1, false); - - // Dummy intent. Do nothing when clicked - PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(), 0); - builder.setContentIntent(intent); - - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(ConstantUtil.NOTIFICATION_DATA_SYNC, builder.build()); + String text = failedForms > 0 ? + String.format(getString(R.string.data_sync_all), syncedForms, failedForms) + : + String.format(getString(R.string.data_sync_synced), syncedForms); + + String title = getString(R.string.data_sync_title); + NotificationHelper.displayNonOngoingNotificationWithProgress(this, text, title, + ConstantUtil.NOTIFICATION_DATA_SYNC); } private void displayFormDeletedNotification(String id, String name) { // Create a unique ID for this form's delete notification - final int notificationId = (int)formId(id); + final int notificationId = formId(id); // Do not show failed if there is none - String text = String.format("Form \"%s\" has been deleted", name); - - NotificationCompat.Builder builder = new NotificationCompat.Builder(this) - .setSmallIcon(R.drawable.info) - .setContentTitle("Form deleted") - .setContentText(text) - .setTicker(text) - .setOngoing(false); - - // Dummy intent. Do nothing when clicked - PendingIntent dummyIntent = PendingIntent.getActivity(this, 0, new Intent(), 0); - builder.setContentIntent(dummyIntent); - - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(notificationId, builder.build()); + String text = String.format(getString(R.string.data_sync_error_form_deleted_text), name); + String title = getString(R.string.data_sync_error_form_deleted_title); + + NotificationHelper.displayNonOnGoingErrorNotification(this, notificationId, text, title); } - private String contentType(String ext) { + private String contentType(@NonNull String ext) { switch (ext) { case ConstantUtil.PNG_SUFFIX: return PNG_CONTENT_TYPE; @@ -764,10 +702,10 @@ private String contentType(String ext) { /** * Coerce a form id into its numeric format */ - public static long formId(String id) { + private static int formId(String id) { try { - return Long.valueOf(id); - } catch (NumberFormatException e ){ + return Integer.valueOf(id); + } catch (NumberFormatException e) { Log.e(TAG, id + " is not a valid form id"); return 0; } @@ -777,12 +715,17 @@ public static long formId(String id) { * Helper class to wrap zip file's meta-data */ class ZipFileData { + + @Nullable String uuid = null; + @Nullable String formId = null; + @Nullable String formName = null; + @Nullable String filename = null; + @Nullable String data = null; - List imagePaths = new ArrayList(); + final List imagePaths = new ArrayList<>(); } - } diff --git a/app/src/main/java/org/akvo/flow/service/ExceptionReportingService.java b/app/src/main/java/org/akvo/flow/service/ExceptionReportingService.java index 3248043a5..5f4b97788 100644 --- a/app/src/main/java/org/akvo/flow/service/ExceptionReportingService.java +++ b/app/src/main/java/org/akvo/flow/service/ExceptionReportingService.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2012 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -16,22 +16,13 @@ package org.akvo.flow.service; -import java.io.File; -import java.io.FilenameFilter; -import java.text.DateFormat; -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.HashMap; -import java.util.Map; -import java.util.Timer; -import java.util.TimerTask; - import android.app.Service; import android.content.Intent; import android.os.Environment; import android.os.IBinder; import android.util.Log; +import org.akvo.flow.BuildConfig; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; import org.akvo.flow.util.ConstantUtil; @@ -41,15 +32,25 @@ import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.StatusUtil; +import java.io.File; +import java.io.FilenameFilter; +import java.text.DateFormat; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.HashMap; +import java.util.Map; +import java.util.Timer; +import java.util.TimerTask; + /** * service that periodically checks for stack trace files on the file system * and, if found, uploads them to the server. After a file is uploaded, the * trace file is deleted from the file system. - * + * * @author Christopher Fagiani */ public class ExceptionReportingService extends Service { - private static final String TAG = "EXCEPTION_REPORTING_SERVICE"; + private static final String TAG = "EXCEPTION_REPORTING"; private static final String EXCEPTION_SERVICE_PATH = "/remoteexception"; private static final String ACTION_PARAM = "action"; private static final String ACTION_VALUE = "saveTrace"; @@ -96,7 +97,7 @@ public int onStartCommand(final Intent intent, int flags, int startid) { database = new SurveyDbAdapter(this); database.open(); deviceId = database.getPreference(ConstantUtil.DEVICE_IDENT_KEY); - version = PlatformUtil.getVersionName(this); + version = BuildConfig.VERSION_NAME; phoneNumber = StatusUtil.getPhoneNumber(this); imei = StatusUtil.getImei(this); } finally { @@ -113,7 +114,8 @@ public int onStartCommand(final Intent intent, int flags, int startid) { @Override public void run() { if (StatusUtil.hasDataConnection(ExceptionReportingService.this) && - Environment.MEDIA_MOUNTED.equals(Environment.getExternalStorageState())) { + Environment.MEDIA_MOUNTED + .equals(Environment.getExternalStorageState())) { submitStackTraces(server); } } @@ -129,7 +131,7 @@ public void onCreate() { /** * Returns all stack trace files on the files system - * + * * @return */ private String[] getTraceFiles(File dir) { @@ -145,7 +147,7 @@ public boolean accept(File dir, String name) { * Look in the trace directory for any files. If any are found, upload to * the server and, on success, delete the file. */ - public void submitStackTraces(String server) { + private void submitStackTraces(String server) { try { File dir = FileUtil.getFilesDir(FileType.STACKTRACE); String[] list = getTraceFiles(dir); @@ -156,7 +158,7 @@ public void submitStackTraces(String server) { // We cannot use the standard FlowApi.getDeviceParams, fot this service uses // a different naming convention... - Map params = new HashMap(); + Map params = new HashMap<>(); params.put(ACTION_PARAM, ACTION_VALUE); params.put(PHONE_PARAM, phoneNumber); params.put(IMEI_PARAM, imei); diff --git a/app/src/main/java/org/akvo/flow/service/LocationService.java b/app/src/main/java/org/akvo/flow/service/LocationService.java deleted file mode 100644 index e34deb880..000000000 --- a/app/src/main/java/org/akvo/flow/service/LocationService.java +++ /dev/null @@ -1,140 +0,0 @@ -/* - * Copyright (C) 2010-2015 Stichting Akvo (Akvo Foundation) - * - * This file is part of Akvo FLOW. - * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. - * - * Akvo FLOW 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 Affero General Public License included below for more details. - * - * The full license text can also be seen at . - */ - -package org.akvo.flow.service; - -import java.io.IOException; -import java.net.URLEncoder; -import java.util.Timer; -import java.util.TimerTask; - -import android.app.Service; -import android.content.Intent; -import android.location.Criteria; -import android.location.Location; -import android.location.LocationManager; -import android.os.IBinder; -import android.util.Log; - -import org.akvo.flow.api.FlowApi; -import org.akvo.flow.dao.SurveyDbAdapter; -import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; -import org.akvo.flow.util.ConstantUtil; -import org.akvo.flow.util.HttpUtil; -import org.akvo.flow.util.StatusUtil; - -/** - * service for sending location beacons on a set interval to the server. This - * can be disabled via the properties menu - * - * @author Christopher Fagiani - */ -public class LocationService extends Service { - private static Timer timer; - private LocationManager locMgr; - private Criteria locationCriteria; - private static final long INITIAL_DELAY = 60000; - private static final long INTERVAL = 1800000; - private static boolean sendBeacon = true; - private static final String BEACON_SERVICE_PATH = "/locationBeacon?action=beacon"; - private static final String LAT = "&lat="; - private static final String LON = "&lon="; - private static final String ACC = "&acc="; - private static final String OS_VERSION = "&osVersion="; - private static final String TAG = "LocationService"; - - public IBinder onBind(Intent intent) { - return null; - } - - /** - * life cycle method for the service. This is called by the system when the - * service is started. It will schedule a timerTask that will periodically - * check the current location and send it to the server - */ - public int onStartCommand(final Intent intent, int flags, int startid) { - // we only need to check this on command start since we'll explicitly - // call endService if they change the preference to false after we're - // already started - SurveyDbAdapter database = null; - final String server = StatusUtil.getServerBase(this); - try { - database = new SurveyDbAdapter(this); - - database.open(); - String val = database.getPreference(ConstantUtil.LOCATION_BEACON_SETTING_KEY); - if (val != null) { - sendBeacon = Boolean.parseBoolean(val); - } - } finally { - if (database != null) { - database.close(); - } - } - // Safe to lazy initialize the static field, since this method - // will always be called in the Main Thread - if (timer == null && sendBeacon) { - timer = new Timer(true); - timer.scheduleAtFixedRate(new TimerTask() { - - @Override - public void run() { - if (sendBeacon && StatusUtil.hasDataConnection(LocationService.this)) { - String provider = locMgr.getBestProvider(locationCriteria, true); - if (provider != null) { - sendLocation(server, locMgr.getLastKnownLocation(provider)); - } - } - } - }, INITIAL_DELAY, INTERVAL); - } - return Service.START_STICKY; - } - - public void onCreate() { - super.onCreate(); - Thread.setDefaultUncaughtExceptionHandler(PersistentUncaughtExceptionHandler.getInstance()); - locMgr = (LocationManager) getSystemService(LOCATION_SERVICE); - locationCriteria = new Criteria(); - locationCriteria.setAccuracy(Criteria.NO_REQUIREMENT); - } - - /** - * sends the location beacon to the server - */ - private void sendLocation(String serverBase, Location loc) { - if (serverBase != null) { - try { - String url = serverBase + BEACON_SERVICE_PATH + "&" + FlowApi.getDeviceParams(); - if (loc != null) { - url += LAT + loc.getLatitude() + LON + loc.getLongitude() + ACC + loc.getAccuracy(); - } - url += OS_VERSION + URLEncoder.encode("Android " + android.os.Build.VERSION.RELEASE); - HttpUtil.httpGet(url); - } catch (IOException e) { - Log.e(TAG, "Could not send location beacon", e); - } - } - } - - public void onDestroy() { - super.onDestroy(); - if (timer != null) { - timer.cancel(); - timer = null; - } - } -} diff --git a/app/src/main/java/org/akvo/flow/service/SurveyDownloadService.java b/app/src/main/java/org/akvo/flow/service/SurveyDownloadService.java index b5d0f3fff..96f589ed0 100644 --- a/app/src/main/java/org/akvo/flow/service/SurveyDownloadService.java +++ b/app/src/main/java/org/akvo/flow/service/SurveyDownloadService.java @@ -1,45 +1,31 @@ /* - * Copyright (C) 2010-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.service; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileNotFoundException; -import java.io.IOException; -import java.io.InputStream; -import java.util.ArrayList; -import java.util.HashSet; -import java.util.List; -import java.util.Set; -import java.util.zip.ZipInputStream; - import android.app.IntentService; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; -import android.support.v4.app.NotificationCompat; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; import org.akvo.flow.R; import org.akvo.flow.api.FlowApi; import org.akvo.flow.api.S3Api; -import org.akvo.flow.serialization.form.SurveyMetaParser; import org.akvo.flow.dao.SurveyDao; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.domain.Question; @@ -53,12 +39,23 @@ import org.akvo.flow.util.FileUtil.FileType; import org.akvo.flow.util.HttpUtil; import org.akvo.flow.util.LangsPreferenceUtil; +import org.akvo.flow.util.NotificationHelper; import org.akvo.flow.util.StatusUtil; -import org.akvo.flow.util.ViewUtil; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.zip.ZipInputStream; /** * This activity will check for new surveys on the device and install as needed - * + * * @author Christopher Fagiani */ public class SurveyDownloadService extends IntentService { @@ -68,22 +65,20 @@ public class SurveyDownloadService extends IntentService { private static final String DEFAULT_TYPE = "Survey"; - private static final String SURVEY_LIST_SERVICE_PATH = "/surveymanager?action=getAvailableSurveysDevice"; - private static final String SURVEY_HEADER_SERVICE_PATH = "/surveymanager?action=getSurveyHeader&surveyId="; - private SurveyDbAdapter databaseAdaptor; public SurveyDownloadService() { super(TAG); } - public void onHandleIntent(Intent intent) { + public void onHandleIntent(@Nullable Intent intent) { if (StatusUtil.hasDataConnection(this)) { try { databaseAdaptor = new SurveyDbAdapter(this); databaseAdaptor.open(); - String[] surveyIds = intent != null ? intent.getStringArrayExtra(EXTRA_SURVEYS) : null; + String[] surveyIds = + intent != null ? intent.getStringArrayExtra(EXTRA_SURVEYS) : null; checkAndDownload(surveyIds); } catch (Exception e) { Log.e(TAG, e.getMessage()); @@ -107,7 +102,7 @@ public void onCreate() { * passed in, then those specific surveys will be downloaded. If they're already * on the device, the surveys will be replaced with the new ones. */ - private void checkAndDownload(String[] surveyIds) throws IOException { + private void checkAndDownload(@Nullable String[] surveyIds) { // Load preferences final String serverBase = StatusUtil.getServerBase(this); @@ -155,7 +150,7 @@ private void checkAndDownload(String[] surveyIds) throws IOException { } } - private void syncSurveyGroups(List surveys) { + private void syncSurveyGroups(@NonNull List surveys) { for (Survey s : surveys) { // Assign registration form id, if missing. SurveyGroup sg = s.getSurveyGroup(); @@ -170,7 +165,7 @@ private void syncSurveyGroups(List surveys) { * Downloads the survey based on the ID and then updates the survey object * with the filename and location */ - private void downloadSurvey(Survey survey) throws IOException { + private void downloadSurvey(@NonNull Survey survey) throws IOException { final String filename = survey.getId() + ConstantUtil.ARCHIVE_SUFFIX; final String objectKey = ConstantUtil.S3_SURVEYS_DIR + filename; final File file = new File(FileUtil.getFilesDir(FileType.FORMS), filename); @@ -191,7 +186,8 @@ private void downloadSurvey(Survey survey) throws IOException { survey.setLocation(ConstantUtil.FILE_LOCATION); } - private Survey loadSurvey(Survey survey) { + @Nullable + private Survey loadSurvey(@NonNull Survey survey) { InputStream in = null; Survey hydratedDurvey = null; try { @@ -218,15 +214,15 @@ private Survey loadSurvey(Survey survey) { /** * checks to see if we should pre-cache help media files (based on the * property in the settings db) and, if we should, downloads the files - * + * * @param survey */ - private void downloadResources(Survey survey) { + private void downloadResources(@NonNull Survey survey) { Survey hydratedSurvey = loadSurvey(survey); if (hydratedSurvey != null) { // collect files in a set just in case the same binary is // used in multiple questions we only need to download once - Set resources = new HashSet(); + Set resources = new HashSet<>(); for (QuestionGroup group : hydratedSurvey.getQuestionGroups()) { for (Question question : group.getQuestions()) { if (!question.getHelpByType(ConstantUtil.VIDEO_HELP_TYPE).isEmpty()) { @@ -247,7 +243,8 @@ private void downloadResources(Survey survey) { } } - private void downloadResources(final String sid, final Set resources) { + private void downloadResources(@NonNull final String sid, + @NonNull final Set resources) { databaseAdaptor.markSurveyHelpDownloaded(sid, false); boolean ok = true; for (String resource : resources) { @@ -256,24 +253,9 @@ private void downloadResources(final String sid, final Set resources) { // Handle both absolute URL (media help files) and S3 object IDs (survey resources) // Naive check to determine whether or not this is an absolute filename if (resource.startsWith("http")) { - final String filename = new File(resource).getName(); - final File surveyDir = new File(FileUtil.getFilesDir(FileType.FORMS), sid); - if (!surveyDir.exists()) { - surveyDir.mkdir(); - } - HttpUtil.httpGet(resource, new File(surveyDir, filename)); + downloadGaeResource(sid, resource); } else { - // resource is just a filename - final String filename = resource + ConstantUtil.ARCHIVE_SUFFIX; - final String objectKey = ConstantUtil.S3_SURVEYS_DIR + filename; - final File resDir = FileUtil.getFilesDir(FileType.RES); - final File file = new File(resDir, filename); - S3Api s3 = new S3Api(SurveyDownloadService.this); - s3.syncFile(objectKey, file); - FileUtil.extract(new ZipInputStream(new FileInputStream(file)), resDir); - if (!file.delete()) { - Log.e(TAG, "Error deleting resource zip file"); - } + downloadS3Resource(resource); } } catch (Exception e) { ok = false; @@ -290,25 +272,46 @@ private void downloadResources(final String sid, final Set resources) { } } + private void downloadS3Resource(String resource) throws IOException { + // resource is just a filename + final String filename = resource + ConstantUtil.ARCHIVE_SUFFIX; + final String objectKey = ConstantUtil.S3_SURVEYS_DIR + filename; + final File resDir = FileUtil.getFilesDir(FileType.RES); + final File file = new File(resDir, filename); + S3Api s3 = new S3Api(SurveyDownloadService.this); + s3.syncFile(objectKey, file); + FileUtil.extract(new ZipInputStream(new FileInputStream(file)), resDir); + if (!file.delete()) { + Log.e(TAG, "Error deleting resource zip file"); + } + } + + private void downloadGaeResource(@NonNull String sid, @NonNull String url) throws IOException { + final String filename = new File(url).getName(); + final File surveyDir = new File(FileUtil.getFilesDir(FileType.FORMS), sid); + if (!surveyDir.exists()) { + surveyDir.mkdir(); + } + HttpUtil.httpGet(url, new File(surveyDir, filename)); + } + /** * invokes a service call to get the header information for multiple surveys */ - private List getSurveyHeaders(String serverBase, String[] surveyIds) { - List surveys = new ArrayList(); + @NonNull + private List getSurveyHeaders(@NonNull String serverBase, @NonNull String[] surveyIds) { + List surveys = new ArrayList<>(); + FlowApi flowApi = new FlowApi(); for (String id : surveyIds) { try { - final String url = serverBase + SURVEY_HEADER_SERVICE_PATH + id + "&" + FlowApi.getDeviceParams(); - String response = HttpUtil.httpGet(url); - if (response != null) { - surveys.addAll(new SurveyMetaParser().parseList(response, true)); - } + surveys.addAll(flowApi.getSurveyHeader(serverBase, id)); } catch (IllegalArgumentException | IOException e) { if (e instanceof IllegalArgumentException) { PersistentUncaughtExceptionHandler.recordException(e); } Log.e(TAG, e.getMessage()); displayErrorNotification(ConstantUtil.NOTIFICATION_HEADER_ERROR, - String.format(getString(R.string.error_form_header), id)); + getString(R.string.error_form_header, id)); } } return surveys; @@ -317,19 +320,16 @@ private List getSurveyHeaders(String serverBase, String[] surveyIds) { /** * invokes a service call to list all surveys that have been designated for * this device (based on phone number). - * + * * @return - an arrayList of Survey objects with the id and version populated * TODO: Move this feature to FLOWApi */ - private List checkForSurveys(String serverBase) { - List surveys = new ArrayList(); + private List checkForSurveys(@NonNull String serverBase) { + List surveys = new ArrayList<>(); + FlowApi api = new FlowApi(); try { - final String url = serverBase + SURVEY_LIST_SERVICE_PATH + "&" + FlowApi.getDeviceParams(); - String response = HttpUtil.httpGet(url); - if (response != null) { - surveys = new SurveyMetaParser().parseList(response); - } - } catch (IllegalArgumentException | IOException e) { + surveys = api.getSurveys(serverBase); + } catch (@NonNull IllegalArgumentException | IOException e) { if (e instanceof IllegalArgumentException) { PersistentUncaughtExceptionHandler.recordException(e); } @@ -341,38 +341,21 @@ private List checkForSurveys(String serverBase) { } private void displayErrorNotification(int id, String msg) { - ViewUtil.displayNotification(getString(R.string.error_form_sync_title), msg, this, id, null); + NotificationHelper + .displayErrorNotification(getString(R.string.error_form_sync_title), msg, this, id); } private void displayNotification(int synced, int failed, int total) { boolean finished = synced + failed >= total; - int icon = finished ? android.R.drawable.stat_sys_download_done - : android.R.drawable.stat_sys_download; - String title = getString(R.string.downloading_forms); // Do not show failed if there is none String text = failed > 0 ? String.format(getString(R.string.data_sync_all), synced, failed) : String.format(getString(R.string.data_sync_synced), synced); - NotificationCompat.Builder builder = new NotificationCompat.Builder(this) - .setSmallIcon(icon) - .setContentTitle(title) - .setContentText(text) - .setTicker(title); - - builder.setOngoing(!finished);// Ongoing if still syncing the records - - // Progress will only be displayed in Android versions > 4.0 - builder.setProgress(total, synced+failed, false); - - // Dummy intent. Do nothing when clicked - PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(), 0); - builder.setContentIntent(intent); - - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(ConstantUtil.NOTIFICATION_FORMS_SYNCED, builder.build()); + NotificationHelper.displayNotification(this, total, title, text, + ConstantUtil.NOTIFICATION_FORMS_SYNCED, !finished, + synced + failed); } /** @@ -380,7 +363,7 @@ private void displayNotification(int synced, int failed, int total) { * This notification will be received in SurveyHomeActivity, in order to * refresh its data */ - public static void sendBroadcastNotification(Context context) { + private static void sendBroadcastNotification(@NonNull Context context) { Intent intentBroadcast = new Intent(context.getString(R.string.action_surveys_sync)); context.sendBroadcast(intentBroadcast); } diff --git a/app/src/main/java/org/akvo/flow/service/SurveyedDataPointSyncService.java b/app/src/main/java/org/akvo/flow/service/SurveyedDataPointSyncService.java new file mode 100644 index 000000000..8a1642585 --- /dev/null +++ b/app/src/main/java/org/akvo/flow/service/SurveyedDataPointSyncService.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2013-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + */ + +package org.akvo.flow.service; + +import android.app.IntentService; +import android.content.Intent; +import android.os.Handler; +import android.support.annotation.NonNull; +import android.support.v4.content.LocalBroadcastManager; +import android.support.v4.util.Pair; +import android.util.Log; + +import org.akvo.flow.R; +import org.akvo.flow.api.FlowApi; +import org.akvo.flow.dao.SurveyDbAdapter; +import org.akvo.flow.domain.SurveyGroup; +import org.akvo.flow.domain.SurveyInstance; +import org.akvo.flow.domain.SurveyedLocale; +import org.akvo.flow.exception.HttpException; +import org.akvo.flow.util.ConstantUtil; +import org.akvo.flow.util.NotificationHelper; +import org.akvo.flow.util.StatusUtil; + +import java.io.IOException; +import java.net.HttpURLConnection; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +public class SurveyedDataPointSyncService extends IntentService { + + private static final String TAG = SurveyedDataPointSyncService.class.getSimpleName(); + + public static final String SURVEY_GROUP = "survey_group"; + + private final Handler mHandler = new Handler(); + + public SurveyedDataPointSyncService() { + super(TAG); + // Tell the system to restart the service if it was unexpectedly stopped before completion + setIntentRedelivery(true); + } + + @Override + protected void onHandleIntent(Intent intent) { + final long surveyGroupId = intent.getLongExtra(SURVEY_GROUP, SurveyGroup.ID_NONE); + int syncedRecords = 0; + FlowApi api = new FlowApi(); + SurveyDbAdapter database = new SurveyDbAdapter(getApplicationContext()).open(); + boolean correctSync = true; + NotificationHelper + .displayNotificationWithProgress(this, getString(R.string.syncing_records), + getString(R.string.pleasewait), true, true, + ConstantUtil.NOTIFICATION_RECORD_SYNC); + try { + Set batch, lastBatch = new HashSet<>(); + while (true) { + Pair, Boolean> syncResult = sync(database, api, surveyGroupId); + batch = syncResult.first; + if (!syncResult.second) { + //at least one of the data points seems corrupted + correctSync = syncResult.second; + } + batch.removeAll(lastBatch);// Remove duplicates. + if (batch.isEmpty()) { + break; + } + syncedRecords += batch.size(); + sendBroadcastNotification();// Keep the UI fresh! + NotificationHelper + .displayNotificationWithProgress(this, getString(R.string.syncing_records), + String.format(getString(R.string.synced_records), + syncedRecords), true, true, + ConstantUtil.NOTIFICATION_RECORD_SYNC); + lastBatch = batch; + } + if (correctSync) { + NotificationHelper + .displayNotificationWithProgress(this, getString(R.string.syncing_records), + String.format(getString(R.string.synced_records), + syncedRecords), false, false, + ConstantUtil.NOTIFICATION_RECORD_SYNC); + } else { + NotificationHelper.displayErrorNotificationWithProgress(this, + getString(R.string.sync_error), + getString(R.string.syncing_corrupted_data_points_error), false, false, + ConstantUtil.NOTIFICATION_RECORD_SYNC); + } + } catch (HttpException e) { + Log.e(TAG, e.getMessage(), e); + String message = e.getMessage(); + switch (e.getStatus()) { + case HttpURLConnection.HTTP_FORBIDDEN: + // A missing assignment might be the issue. Let's hint the user. + message = getString(R.string.error_assignment_text); + break; + } + displayToast(message); + NotificationHelper + .displayErrorNotificationWithProgress(this, getString(R.string.sync_error), + message, false, + false, ConstantUtil.NOTIFICATION_RECORD_SYNC); + } catch (IOException e) { + Log.e(TAG, e.getMessage(), e); + displayToast(getString(R.string.network_error)); + NotificationHelper + .displayErrorNotificationWithProgress(this, getString(R.string.sync_error), + getString(R.string.network_error), false, false, + ConstantUtil.NOTIFICATION_RECORD_SYNC); + } finally { + database.close(); + } + + sendBroadcastNotification(); + } + + /** + * Sync a Record batch, and return the Set of Record IDs within the response + */ + @NonNull + private Pair, Boolean> sync(@NonNull SurveyDbAdapter database, @NonNull FlowApi api, + long surveyGroupId) + throws IOException { + final String syncTime = database.getSyncTime(surveyGroupId); + Set records = new HashSet<>(); + Log.d(TAG, "sync() - SurveyGroup: " + surveyGroupId + ". SyncTime: " + syncTime); + List locales = api + .getSurveyedLocales(StatusUtil.getServerBase(this), surveyGroupId, syncTime); + boolean correctData = true; + if (locales != null) { + for (SurveyedLocale locale : locales) { + List surveyInstances = locale.getSurveyInstances(); + if (surveyInstances == null || surveyInstances.isEmpty()) { + correctData = false; + } + database.syncSurveyedLocale(locale); + records.add(locale.getId()); + } + } + //Delete empty or corrupted data received from server + database.deleteEmptyRecords(); + return new Pair<>(records, correctData); + } + + private void displayToast(final String text) { + mHandler.post(new ServiceToastRunnable(getApplicationContext(), text)); + } + + /** + * Dispatch a Broadcast notification to notify of SurveyedLocales synchronization. + * This notification will be received in {@link org.akvo.flow.ui.fragment.DatapointsFragment}, in order to + * refresh its data + */ + private void sendBroadcastNotification() { + Intent intentBroadcast = new Intent(ConstantUtil.ACTION_LOCALE_SYNC); + LocalBroadcastManager.getInstance(this).sendBroadcast(intentBroadcast); + } +} diff --git a/app/src/main/java/org/akvo/flow/service/SurveyedLocaleSyncService.java b/app/src/main/java/org/akvo/flow/service/SurveyedLocaleSyncService.java deleted file mode 100644 index 30ab200d2..000000000 --- a/app/src/main/java/org/akvo/flow/service/SurveyedLocaleSyncService.java +++ /dev/null @@ -1,162 +0,0 @@ -/* - * Copyright (C) 2013-2016 Stichting Akvo (Akvo Foundation) - * - * This file is part of Akvo FLOW. - * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. - * - * Akvo FLOW 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 Affero General Public License included below for more details. - * - * The full license text can also be seen at . - */ - -package org.akvo.flow.service; - -import java.io.IOException; -import java.util.HashSet; -import java.util.List; -import java.util.Set; - -import android.app.IntentService; -import android.app.NotificationManager; -import android.app.PendingIntent; -import android.content.Context; -import android.content.Intent; -import android.os.Handler; -import android.support.v4.app.NotificationCompat; -import android.util.Log; -import android.widget.Toast; - -import org.akvo.flow.R; -import org.akvo.flow.api.FlowApi; -import org.akvo.flow.dao.SurveyDbAdapter; -import org.akvo.flow.domain.SurveyGroup; -import org.akvo.flow.domain.SurveyedLocale; -import org.akvo.flow.exception.HttpException; -import org.akvo.flow.util.ConstantUtil; - -public class SurveyedLocaleSyncService extends IntentService { - private static final String TAG = SurveyedLocaleSyncService.class.getSimpleName(); - - public static final String SURVEY_GROUP = "survey_group"; - - private Handler mHandler = new Handler(); - - public SurveyedLocaleSyncService() { - super(TAG); - // Tell the system to restart the service if it was unexpectedly stopped before completion - setIntentRedelivery(true); - } - - @Override - protected void onHandleIntent(Intent intent) { - final long surveyGroupId = intent.getLongExtra(SURVEY_GROUP, SurveyGroup.ID_NONE); - int syncedRecords = 0; - FlowApi api = new FlowApi(); - SurveyDbAdapter database = new SurveyDbAdapter(getApplicationContext()).open(); - displayNotification(getString(R.string.syncing_records), - getString(R.string.pleasewait), false); - try { - Set batch, lastBatch = new HashSet<>(); - while (true) { - batch = sync(database, api, surveyGroupId); - batch.removeAll(lastBatch);// Remove duplicates. - if (batch.isEmpty()) { - break; - } - syncedRecords += batch.size(); - sendBroadcastNotification();// Keep the UI fresh! - displayNotification(getString(R.string.syncing_records), - String.format(getString(R.string.synced_records), syncedRecords), false); - lastBatch = batch; - } - displayNotification(getString(R.string.sync_finished), - String.format(getString(R.string.synced_records), syncedRecords), true); - } catch (HttpException e) { - Log.e(TAG, e.getMessage()); - String message = e.getMessage(); - switch (e.getStatus()) { - case HttpException.Status.SC_FORBIDDEN: - // A missing assignment might be the issue. Let's hint the user. - message = getString(R.string.error_assignment_text); - break; - } - displayToast(message); - displayNotification(getString(R.string.sync_error), message, true); - } catch (IOException e) { - Log.e(TAG, e.getMessage()); - displayToast(getString(R.string.network_error)); - displayNotification(getString(R.string.sync_error), - getString(R.string.network_error), true); - } finally { - database.close(); - } - - sendBroadcastNotification(); - } - - /** - * Sync a Record batch, and return the Set of Record IDs within the response - */ - private Set sync(SurveyDbAdapter database, FlowApi api, long surveyGroupId) - throws IOException { - final String syncTime = database.getSyncTime(surveyGroupId); - Set records = new HashSet(); - Log.d(TAG, "sync() - SurveyGroup: " + surveyGroupId + ". SyncTime: " + syncTime); - List locales = api.getSurveyedLocales(surveyGroupId, syncTime); - if (locales != null) { - for (SurveyedLocale locale : locales) { - database.syncSurveyedLocale(locale); - records.add(locale.getId()); - } - } - return records; - } - - private void displayToast(final String text) { - mHandler.post(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), text, Toast.LENGTH_LONG).show(); - } - }); - } - - private void displayNotification(String title, String text, boolean finished) { - int icon = finished ? android.R.drawable.stat_sys_download_done - : android.R.drawable.stat_sys_download; - NotificationCompat.Builder builder = new NotificationCompat.Builder(this) - .setSmallIcon(icon) - .setContentTitle(title) - .setContentText(text) - .setTicker(title); - - builder.setOngoing(!finished);// Ongoing if still syncing the records - - // Progress will only be displayed in Android versions > 4.0 - builder.setProgress(1, 1, !finished); - - // Dummy intent. Do nothing when clicked - PendingIntent intent = PendingIntent.getActivity(this, 0, new Intent(), 0); - builder.setContentIntent(intent); - - NotificationManager notificationManager = - (NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(ConstantUtil.NOTIFICATION_RECORD_SYNC, builder.build()); - } - - /** - * Dispatch a Broadcast notification to notify of SurveyedLocales synchronization. - * This notification will be received in SurveyedLocalesActivity, in order to - * refresh its data - */ - private void sendBroadcastNotification() { - Intent intentBroadcast = new Intent(getString(R.string.action_locales_sync)); - sendBroadcast(intentBroadcast); - } - -} diff --git a/app/src/main/java/org/akvo/flow/service/TimeCheckService.java b/app/src/main/java/org/akvo/flow/service/TimeCheckService.java index 671bc7acf..82c29149d 100644 --- a/app/src/main/java/org/akvo/flow/service/TimeCheckService.java +++ b/app/src/main/java/org/akvo/flow/service/TimeCheckService.java @@ -18,15 +18,12 @@ import android.app.IntentService; import android.content.Intent; -import android.text.TextUtils; import android.util.Log; import org.akvo.flow.activity.TimeCheckActivity; +import org.akvo.flow.api.FlowApi; import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; -import org.akvo.flow.util.HttpUtil; import org.akvo.flow.util.StatusUtil; -import org.json.JSONException; -import org.json.JSONObject; import java.io.IOException; import java.text.DateFormat; @@ -34,9 +31,10 @@ import java.text.SimpleDateFormat; import java.util.TimeZone; +import static org.akvo.flow.util.StringUtil.isValid; + public class TimeCheckService extends IntentService { private static final String TAG = TimeCheckService.class.getSimpleName(); - private static final String TIME_CHECK_PATH = "/devicetimerest"; private static final long OFFSET_THRESHOLD = 13 * 60 * 1000;// 13 minutes private static final String PATTERN = "yyyy-MM-dd'T'HH:mm:ss'Z'";// ISO 8601 private static final String TIMEZONE = "UTC"; @@ -59,34 +57,25 @@ private void checkTime() { // Since a misconfigured date/time might be considering the SSL certificate as expired, // we'll use HTTP by default, instead of HTTPS - String serverBase = StatusUtil.getServerBase(this); - if (serverBase.startsWith("https")) { - serverBase = "http" + serverBase.substring("https".length()); - } - try { - final String url = serverBase + TIME_CHECK_PATH + "?ts=" + System.currentTimeMillis(); - String response = HttpUtil.httpGet(url); - if (!TextUtils.isEmpty(response)) { - JSONObject json = new JSONObject(response); - String time = json.getString("time"); - if (!TextUtils.isEmpty(time) && !time.equalsIgnoreCase("null")) { - DateFormat df = new SimpleDateFormat(PATTERN); - df.setTimeZone(TimeZone.getTimeZone(TIMEZONE)); - final long remote = df.parse(time).getTime(); - final long local = System.currentTimeMillis(); - boolean onTime = Math.abs(remote - local) < OFFSET_THRESHOLD; + FlowApi flowApi = new FlowApi(); + String time = flowApi.getServerTime(StatusUtil.getServerBase(this)); - if (!onTime) { - Intent i = new Intent(this, TimeCheckActivity.class); - i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); - startActivity(i); - } + if (isValid(time)) { + DateFormat df = new SimpleDateFormat(PATTERN); + df.setTimeZone(TimeZone.getTimeZone(TIMEZONE)); + final long remote = df.parse(time).getTime(); + final long local = System.currentTimeMillis(); + boolean onTime = Math.abs(remote - local) < OFFSET_THRESHOLD; + + if (!onTime) { + Intent i = new Intent(this, TimeCheckActivity.class); + i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(i); } } - } catch (IOException | JSONException | ParseException e) { + } catch (IOException | ParseException e) { Log.e(TAG, "Error fetching time: ", e); } } - } diff --git a/app/src/main/java/org/akvo/flow/service/UserRequestedApkUpdateService.java b/app/src/main/java/org/akvo/flow/service/UserRequestedApkUpdateService.java index 128e56980..90c6cabcd 100644 --- a/app/src/main/java/org/akvo/flow/service/UserRequestedApkUpdateService.java +++ b/app/src/main/java/org/akvo/flow/service/UserRequestedApkUpdateService.java @@ -19,10 +19,14 @@ import android.app.IntentService; import android.content.Intent; import android.os.Handler; +import android.support.v4.util.Pair; import android.util.Log; + import org.akvo.flow.R; import org.akvo.flow.activity.AppUpdateActivity; +import org.akvo.flow.domain.apkupdate.ViewApkData; import org.akvo.flow.exception.PersistentUncaughtExceptionHandler; +import org.akvo.flow.ui.Navigator; import org.akvo.flow.util.StatusUtil; import org.akvo.flow.util.ViewUtil; @@ -39,6 +43,7 @@ public class UserRequestedApkUpdateService extends IntentService { private static final String TAG = "USER_REQ_APK_UPDATE"; private final ApkUpdateHelper apkUpdateHelper = new ApkUpdateHelper(); + private final Navigator navigator = new Navigator(); public UserRequestedApkUpdateService() { super(TAG); @@ -70,7 +75,11 @@ private void checkUpdates() { } try { - if (!apkUpdateHelper.shouldUpdate(this)) { + Pair booleanApkDataPair = apkUpdateHelper.shouldUpdate(this); + if (booleanApkDataPair.first) { + // There is a newer version. Fire the 'Download and Install' Activity. + navigator.navigateToAppUpdate(this, booleanApkDataPair.second); + } else { ViewUtil.displayToastFromService(getString(R.string.apk_update_service_no_update), uiHandler, getApplicationContext()); } diff --git a/app/src/main/java/org/akvo/flow/ui/Navigator.java b/app/src/main/java/org/akvo/flow/ui/Navigator.java index dedbc1108..774b9d483 100644 --- a/app/src/main/java/org/akvo/flow/ui/Navigator.java +++ b/app/src/main/java/org/akvo/flow/ui/Navigator.java @@ -1,18 +1,29 @@ package org.akvo.flow.ui; +import android.app.Activity; import android.content.Context; import android.content.Intent; +import android.os.Bundle; import android.support.annotation.NonNull; + +import org.akvo.flow.activity.AddUserActivity; import org.akvo.flow.activity.AppUpdateActivity; -import org.akvo.flow.domain.apkupdate.ApkData; +import org.akvo.flow.activity.FormActivity; +import org.akvo.flow.activity.RecordActivity; +import org.akvo.flow.domain.SurveyGroup; +import org.akvo.flow.domain.User; +import org.akvo.flow.domain.apkupdate.ViewApkData; +import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.StringUtil; +import static org.akvo.flow.util.ConstantUtil.REQUEST_ADD_USER; + public class Navigator { public Navigator() { } - public void navigateToAppUpdate(@NonNull Context context, @NonNull ApkData data) { + public void navigateToAppUpdate(@NonNull Context context, @NonNull ViewApkData data) { Intent i = new Intent(context, AppUpdateActivity.class); i.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK); i.putExtra(AppUpdateActivity.EXTRA_URL, data.getFileUrl()); @@ -23,4 +34,33 @@ public void navigateToAppUpdate(@NonNull Context context, @NonNull ApkData data) } context.startActivity(i); } + + public void navigateToAddUser(Activity activity) { + activity.startActivityForResult(new Intent(activity, AddUserActivity.class), + REQUEST_ADD_USER); + } + + public void navigateToRecordActivity(Context context, String surveyedLocaleId, + SurveyGroup mSurveyGroup) { + // Display form list and history + Intent intent = new Intent(context, RecordActivity.class); + Bundle extras = new Bundle(); + extras.putSerializable(RecordActivity.EXTRA_SURVEY_GROUP, mSurveyGroup); + extras.putString(RecordActivity.EXTRA_RECORD_ID, surveyedLocaleId); + intent.putExtras(extras); + context.startActivity(intent); + } + + public void navigateToFormActivity(Context context, String surveyedLocaleId, User user, + String formId, + long formInstanceId, boolean readOnly, SurveyGroup mSurveyGroup) { + Intent i = new Intent(context, FormActivity.class); + i.putExtra(ConstantUtil.USER_ID_KEY, user.getId()); + i.putExtra(ConstantUtil.SURVEY_ID_KEY, formId); + i.putExtra(ConstantUtil.SURVEY_GROUP, mSurveyGroup); + i.putExtra(ConstantUtil.SURVEYED_LOCALE_ID, surveyedLocaleId); + i.putExtra(ConstantUtil.RESPONDENT_ID_KEY, formInstanceId); + i.putExtra(ConstantUtil.READONLY_KEY, readOnly); + context.startActivity(i); + } } \ No newline at end of file diff --git a/app/src/main/java/org/akvo/flow/ui/adapter/SurveyTabAdapter.java b/app/src/main/java/org/akvo/flow/ui/adapter/SurveyTabAdapter.java index 7e2780746..3df7183a3 100644 --- a/app/src/main/java/org/akvo/flow/ui/adapter/SurveyTabAdapter.java +++ b/app/src/main/java/org/akvo/flow/ui/adapter/SurveyTabAdapter.java @@ -13,6 +13,7 @@ * * The full license text can also be seen at . */ + package org.akvo.flow.ui.adapter; import android.content.Context; @@ -37,8 +38,9 @@ import java.util.ArrayList; import java.util.List; -public class SurveyTabAdapter extends PagerAdapter implements ViewPager.OnPageChangeListener, - ActionBar.TabListener { +public class SurveyTabAdapter extends PagerAdapter + implements ViewPager.OnPageChangeListener, ActionBar.TabListener { + private static final String TAG = SurveyTabAdapter.class.getSimpleName(); private Context mContext; @@ -52,7 +54,8 @@ public class SurveyTabAdapter extends PagerAdapter implements ViewPager.OnPageCh private SubmitTab mSubmitTab; public SurveyTabAdapter(Context context, ActionBar actionBar, ViewPager pager, - SurveyListener surveyListener, QuestionInteractionListener questionListener) { + SurveyListener surveyListener, + QuestionInteractionListener questionListener) { mContext = context; mSurveyListener = surveyListener; mQuestionListener = questionListener; @@ -63,11 +66,11 @@ public SurveyTabAdapter(Context context, ActionBar actionBar, ViewPager pager, private void init() { mQuestionGroups = mSurveyListener.getQuestionGroups(); - mQuestionGroupTabs = new ArrayList(); + mQuestionGroupTabs = new ArrayList<>(); for (QuestionGroup group : mQuestionGroups) { - QuestionGroupTab questionGroupTab = new QuestionGroupTab(mContext, group, - mSurveyListener, mQuestionListener); + QuestionGroupTab questionGroupTab = + new QuestionGroupTab(mContext, group, mSurveyListener, mQuestionListener); mQuestionGroupTabs.add(questionGroupTab); } @@ -79,14 +82,11 @@ private void init() { mActionBar.setNavigationMode(ActionBar.NAVIGATION_MODE_TABS); for (QuestionGroup group : mQuestionGroups) { - mActionBar.addTab(mActionBar.newTab() - .setText(group.getHeading()) - .setTabListener(this)); + mActionBar.addTab(mActionBar.newTab().setText(group.getHeading()).setTabListener(this)); } if (mSubmitTab != null) { - mActionBar.addTab(mActionBar.newTab() - .setText(R.string.submitbutton) + mActionBar.addTab(mActionBar.newTab().setText(R.string.submitbutton) .setTabListener(this)); } @@ -127,7 +127,7 @@ public void reset() { * Upon success the tab position will be returned, -1 otherwise */ public int displayQuestion(String questionId) { - for (int i=0; i checkInvalidQuestions() { return invalidQuestions; } - } diff --git a/app/src/main/java/org/akvo/flow/ui/fragment/DatapointsFragment.java b/app/src/main/java/org/akvo/flow/ui/fragment/DatapointsFragment.java index 7c1b5a6e0..400104eb3 100644 --- a/app/src/main/java/org/akvo/flow/ui/fragment/DatapointsFragment.java +++ b/app/src/main/java/org/akvo/flow/ui/fragment/DatapointsFragment.java @@ -1,28 +1,31 @@ /* - * Copyright (C) 2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ + package org.akvo.flow.ui.fragment; +import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentPagerAdapter; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.ViewPager; import android.util.Log; import android.view.LayoutInflater; @@ -32,7 +35,6 @@ import android.view.SubMenu; import android.view.View; import android.view.ViewGroup; -import android.widget.Toast; import com.astuetz.PagerSlidingTabStrip; @@ -40,22 +42,40 @@ import org.akvo.flow.activity.SurveyActivity; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.domain.SurveyGroup; -import org.akvo.flow.service.SurveyedLocaleSyncService; +import org.akvo.flow.util.ConstantUtil; + +import java.lang.ref.WeakReference; +import java.util.Map; +import java.util.WeakHashMap; public class DatapointsFragment extends Fragment { + private static final String TAG = DatapointsFragment.class.getSimpleName(); private static final int POSITION_LIST = 0; private static final int POSITION_MAP = 1; + private static final String STATS_DIALOG_FRAGMENT_TAG = "stats"; + + /** + * BroadcastReceiver to notify of records synchronisation. This should be + * fired from SurveyedLocalesSyncService. + */ + private final BroadcastReceiver mSurveyedLocalesSyncReceiver = new DataPointSyncBroadcastReceiver( + this); private SurveyDbAdapter mDatabase; private TabsAdapter mTabsAdapter; private ViewPager mPager; - - private String[] mTabs; private SurveyGroup mSurveyGroup; - public static DatapointsFragment instantiate(SurveyGroup surveyGroup) { + @Nullable + private DatapointFragmentListener listener; + private String[] tabNames; + + public DatapointsFragment() { + } + + public static DatapointsFragment newInstance(SurveyGroup surveyGroup) { DatapointsFragment fragment = new DatapointsFragment(); Bundle args = new Bundle(); args.putSerializable(SurveyActivity.EXTRA_SURVEY_GROUP, surveyGroup); @@ -63,17 +83,27 @@ public static DatapointsFragment instantiate(SurveyGroup surveyGroup) { return fragment; } + @Override + public void onAttach(Activity activity) { + super.onAttach(activity); + if (!(activity instanceof DatapointFragmentListener)) { + throw new IllegalArgumentException("Activity must implement DatapointFragmentListener"); + } + this.listener = (DatapointFragmentListener) activity; + } + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mSurveyGroup = (SurveyGroup) getArguments().getSerializable(SurveyActivity.EXTRA_SURVEY_GROUP); - mTabs = getResources().getStringArray(R.array.records_activity_tabs); + mSurveyGroup = (SurveyGroup) getArguments() + .getSerializable(SurveyActivity.EXTRA_SURVEY_GROUP); + tabNames = getResources().getStringArray(R.array.records_activity_tabs); setHasOptionsMenu(true); setRetainInstance(true); } @Override - public void onActivityCreated (Bundle savedInstanceState) { + public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); if (mDatabase == null) { @@ -82,6 +112,12 @@ public void onActivityCreated (Bundle savedInstanceState) { } } + @Override + public void onDetach() { + super.onDetach(); + this.listener = null; + } + @Override public void onDestroy() { super.onDestroy(); @@ -96,27 +132,29 @@ public void onResume() { // TODO: providing the id to RecordActivity, and reading it back on onActivityResult(...) mDatabase.deleteEmptyRecords(); - getActivity().registerReceiver(mSurveyedLocalesSyncReceiver, - new IntentFilter(getString(R.string.action_locales_sync))); + LocalBroadcastManager.getInstance(getActivity()) + .registerReceiver(mSurveyedLocalesSyncReceiver, + new IntentFilter(ConstantUtil.ACTION_LOCALE_SYNC)); } @Override public void onPause() { super.onPause(); - getActivity().unregisterReceiver(mSurveyedLocalesSyncReceiver); + LocalBroadcastManager.getInstance(getActivity()) + .unregisterReceiver(mSurveyedLocalesSyncReceiver); } @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { View v = inflater.inflate(R.layout.datapoints_fragment, container, false); - mPager = (ViewPager)v.findViewById(R.id.pager); + mPager = (ViewPager) v.findViewById(R.id.pager); PagerSlidingTabStrip tabs = (PagerSlidingTabStrip) v.findViewById(R.id.tabs); // Init tabs - mTabsAdapter = new TabsAdapter(getFragmentManager()); + mTabsAdapter = new TabsAdapter(getFragmentManager(), tabNames, mSurveyGroup); mPager.setAdapter(mTabsAdapter); tabs.setViewPager(mPager); - //tabs.setOnPageChangeListener(mTabsAdapter); return v; } @@ -137,6 +175,7 @@ public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { // the Tab index, not the Pager one, which turns out to be buggy in some Android versions. // TODO: If this approach is still unreliable, we'll need to invalidate the menu twice. if (mPager != null && mPager.getCurrentItem() == POSITION_MAP) { + //TODO: maybe instead of removing we should use custom menu for each fragment subMenu.removeItem(R.id.order_by); } } @@ -148,89 +187,131 @@ public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection switch (item.getItemId()) { case R.id.new_datapoint: - String newLocaleId = mDatabase.createSurveyedLocale(mSurveyGroup.getId()); - ((SurveyActivity)getActivity()).onRecordSelected(newLocaleId);// TODO: Use interface pattern + if (listener != null) { + String newLocaleId = mDatabase.createSurveyedLocale(mSurveyGroup.getId()); + listener.onRecordSelected(newLocaleId); + } return true; case R.id.search: - return getActivity().onSearchRequested(); + if (listener != null) { + return listener.onSearchTap(); + } case R.id.sync_records: - Toast.makeText(getActivity(), R.string.syncing_records, Toast.LENGTH_SHORT).show(); - Intent intent = new Intent(getActivity(), SurveyedLocaleSyncService.class); - intent.putExtra(SurveyedLocaleSyncService.SURVEY_GROUP, mSurveyGroup.getId()); - getActivity().startService(intent); + if (listener != null && mSurveyGroup != null) { + listener.onSyncRecordsTap(mSurveyGroup.getId()); + } return true; case R.id.stats: - StatsDialogFragment dialogFragment = StatsDialogFragment.newInstance(mSurveyGroup.getId()); - dialogFragment.show(getFragmentManager(), "stats"); + StatsDialogFragment dialogFragment = StatsDialogFragment + .newInstance(mSurveyGroup.getId()); + dialogFragment.show(getFragmentManager(), STATS_DIALOG_FRAGMENT_TAG); return true; default: return super.onOptionsItemSelected(item); } } - class TabsAdapter extends FragmentPagerAdapter { + private static class TabsAdapter extends FragmentPagerAdapter { + + private final String[] tabs; + private SurveyGroup surveyGroup; + private final Map fragmentsRef = new WeakHashMap<>(2); - public TabsAdapter(FragmentManager fm) { + public TabsAdapter(FragmentManager fm, String[] tabs, SurveyGroup surveyGroup) { super(fm); + this.tabs = tabs; + this.surveyGroup = surveyGroup; } @Override public int getCount() { - return mTabs.length; - } - - private Fragment getFragment(int pos) { - // Hell of a hack. This should be changed for a more reliable method - String tag = "android:switcher:" + R.id.pager + ":" + pos; - return getFragmentManager().findFragmentByTag(tag); + return tabs.length; } - public void refreshFragments() { - SurveyedLocaleListFragment listFragment = (SurveyedLocaleListFragment) getFragment(POSITION_LIST); - MapFragment mapFragment = (MapFragment) getFragment(POSITION_MAP); + public void refreshFragments(SurveyGroup newSurveyGroup) { + this.surveyGroup = newSurveyGroup; + SurveyedLocaleListFragment listFragment = (SurveyedLocaleListFragment) fragmentsRef + .get(POSITION_LIST); + MapFragment mapFragment = (MapFragment) fragmentsRef.get(POSITION_MAP); if (listFragment != null) { - listFragment.refresh(mSurveyGroup); + listFragment.refresh(surveyGroup); } if (mapFragment != null) { - mapFragment.refresh(mSurveyGroup); + mapFragment.refresh(surveyGroup); + } + } + + @Override + public Object instantiateItem(ViewGroup container, int position) { + if (position == POSITION_LIST) { + SurveyedLocaleListFragment surveyedLocaleListFragment = (SurveyedLocaleListFragment) super + .instantiateItem(container, position); + fragmentsRef.put(POSITION_LIST, surveyedLocaleListFragment); + return surveyedLocaleListFragment; + } else { + MapFragment mapFragment = (MapFragment) super.instantiateItem(container, position); + fragmentsRef.put(POSITION_MAP, mapFragment); + return mapFragment; } } @Override public Fragment getItem(int position) { if (position == POSITION_LIST) { - return SurveyedLocaleListFragment.newInstance(mSurveyGroup); + return SurveyedLocaleListFragment.newInstance(surveyGroup); } // Map mode - return MapFragment.newInstance(mSurveyGroup, null); + return MapFragment.newInstance(surveyGroup, null); } @Override public CharSequence getPageTitle(int position) { - return mTabs[position]; + return tabs[position]; } } public void refresh(SurveyGroup surveyGroup) { mSurveyGroup = surveyGroup; + refreshView(); + } + + private void refreshView() { if (mTabsAdapter != null) { - mTabsAdapter.refreshFragments(); + mTabsAdapter.refreshFragments(mSurveyGroup); + } + if (listener != null) { + listener.refreshMenu(); } - getActivity().supportInvalidateOptionsMenu(); } - /** - * BroadcastReceiver to notify of records synchronisation. This should be - * fired from SurveyedLocalesSyncService. - */ - private BroadcastReceiver mSurveyedLocalesSyncReceiver = new BroadcastReceiver() { + private static class DataPointSyncBroadcastReceiver extends BroadcastReceiver { + + private final WeakReference fragmentWeakReference; + + private DataPointSyncBroadcastReceiver(DatapointsFragment fragment) { + this.fragmentWeakReference = new WeakReference<>(fragment); + } + @Override public void onReceive(Context context, Intent intent) { Log.d(TAG, "New Records have been synchronised. Refreshing fragments..."); - refresh(mSurveyGroup); + DatapointsFragment datapointsFragment = fragmentWeakReference.get(); + if (datapointsFragment != null) { + datapointsFragment.refreshView(); + } } - }; + } + public interface DatapointFragmentListener { + + void refreshMenu(); + + void onRecordSelected(String recordId); + + boolean onSearchTap(); + + void onSyncRecordsTap(long surveyGroupId); + } } diff --git a/app/src/main/java/org/akvo/flow/ui/fragment/DrawerFragment.java b/app/src/main/java/org/akvo/flow/ui/fragment/DrawerFragment.java index 51d527d88..74348da3d 100644 --- a/app/src/main/java/org/akvo/flow/ui/fragment/DrawerFragment.java +++ b/app/src/main/java/org/akvo/flow/ui/fragment/DrawerFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -13,17 +13,21 @@ * * The full license text can also be seen at . */ + package org.akvo.flow.ui.fragment; import android.app.Activity; import android.app.AlertDialog; +import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.graphics.Color; import android.os.Bundle; +import android.support.annotation.ColorInt; import android.support.v4.app.Fragment; import android.support.v4.app.LoaderManager; +import android.support.v4.content.ContextCompat; import android.support.v4.content.Loader; import android.text.TextUtils; import android.view.ContextMenu; @@ -87,7 +91,8 @@ public interface DrawerListener { private List mSurveys = new ArrayList<>(); @Override - public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { + public View onCreateView(LayoutInflater inflater, ViewGroup container, + Bundle savedInstanceState) { View v = inflater.inflate(R.layout.navigation_drawer, container, false); mListView = (ExpandableListView) v.findViewById(R.id.list); @@ -103,7 +108,7 @@ public void onActivityCreated(Bundle savedInstanceState) { mDatabase.open(); } if (mAdapter == null) { - mAdapter = new DrawerAdapter(); + mAdapter = new DrawerAdapter(getActivity()); mListView.setAdapter(mAdapter); mListView.expandGroup(GROUP_SURVEYS); mListView.setOnGroupClickListener(mAdapter); @@ -145,8 +150,9 @@ public void load() { if (!isResumed()) { return; } - getLoaderManager().restartLoader(LOADER_SURVEYS, null, this); - getLoaderManager().restartLoader(LOADER_USERS, null, this); + LoaderManager loaderManager = getLoaderManager(); + loaderManager.restartLoader(LOADER_SURVEYS, null, this); + loaderManager.restartLoader(LOADER_USERS, null, this); } public void onDrawerClosed() { @@ -184,8 +190,10 @@ public void onLoadFinished(Loader loader, Cursor cursor) { mUsers.clear(); if (cursor.moveToFirst()) { do { - long id = cursor.getLong(cursor.getColumnIndexOrThrow(SurveyDbAdapter.UserColumns._ID)); - String name = cursor.getString(cursor.getColumnIndexOrThrow(SurveyDbAdapter.UserColumns.NAME)); + long id = cursor.getLong( + cursor.getColumnIndexOrThrow(SurveyDbAdapter.UserColumns._ID)); + String name = cursor.getString( + cursor.getColumnIndexOrThrow(SurveyDbAdapter.UserColumns.NAME)); User user = new User(id, name); // Skip selected user if (!user.equals(FlowApp.getApp().getUser())) { @@ -226,7 +234,8 @@ public void onClick(DialogInterface dialog, int which) { String name = et.getText().toString();// TODO: Validate name if (TextUtils.isEmpty(name)) { // Disallow blank usernames - Toast.makeText(getActivity(), R.string.empty_user_warning, Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.empty_user_warning, + Toast.LENGTH_SHORT).show(); return; } @@ -247,7 +256,8 @@ public void onClick(DialogInterface dialog, int which) { private void deleteUser(final User user) { final long uid = user.getId(); - ViewUtil.showConfirmDialog(R.string.delete_user, R.string.delete_user_confirmation, getActivity(), + ViewUtil.showConfirmDialog(R.string.delete_user, R.string.delete_user_confirmation, + getActivity(), true, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { @@ -261,7 +271,8 @@ public void onClick(DialogInterface dialog, int which) { } private SurveyGroup getSurveyForContextMenu(int type, int group, int child) { - if (group == GROUP_SURVEYS && type == ExpandableListView.PACKED_POSITION_TYPE_CHILD && child < mSurveys.size()) { + if (group == GROUP_SURVEYS && type == ExpandableListView.PACKED_POSITION_TYPE_CHILD + && child < mSurveys.size()) { return mSurveys.get(child); } return null; @@ -279,7 +290,8 @@ private User getUserForContextMenu(int type, int group, int child) { } @Override - public void onCreateContextMenu(ContextMenu menu, View v, ContextMenu.ContextMenuInfo menuInfo) { + public void onCreateContextMenu(ContextMenu menu, View v, + ContextMenu.ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); ExpandableListContextMenuInfo info = (ExpandableListContextMenuInfo) menuInfo; int type = ExpandableListView.getPackedPositionType(info.packedPosition); @@ -371,13 +383,14 @@ class DrawerAdapter extends BaseExpandableListAdapter implements ExpandableListView.OnGroupClickListener, ExpandableListView.OnChildClickListener { LayoutInflater mInflater; - int mHighlightColor; + @ColorInt + private final int mHighlightColor; - public DrawerAdapter() { - mInflater = LayoutInflater.from(getActivity()); + public DrawerAdapter(Context context) { + mInflater = LayoutInflater.from(context); mUsers = new ArrayList<>(); mSurveys = new ArrayList<>(); - mHighlightColor = PlatformUtil.getResource(getActivity(), R.attr.textColorSecondary); + mHighlightColor = ContextCompat.getColor(context, R.color.orange_main); } @Override @@ -423,7 +436,8 @@ public boolean hasStableIds() { } @Override - public View getGroupView(int groupPosition, boolean isExpanded, View convertView, ViewGroup parent) { + public View getGroupView(int groupPosition, boolean isExpanded, View convertView, + ViewGroup parent) { View v = convertView; if (v == null) { v = mInflater.inflate(R.layout.drawer_item, null); @@ -433,7 +447,6 @@ public View getGroupView(int groupPosition, boolean isExpanded, View convertView ImageView img = (ImageView) v.findViewById(R.id.item_img); ImageView dropdown = (ImageView) v.findViewById(R.id.dropdown); - switch (groupPosition) { case GROUP_USERS: divider.setMinimumHeight(0); @@ -446,13 +459,15 @@ public View getGroupView(int groupPosition, boolean isExpanded, View convertView img.setImageResource(R.drawable.ic_account_circle_black_48dp); img.setVisibility(View.VISIBLE); - dropdown.setImageResource(isExpanded ? R.drawable.ic_action_collapse : R.drawable.ic_action_expand); + dropdown.setImageResource(isExpanded ? + R.drawable.ic_action_collapse : + R.drawable.ic_action_expand); dropdown.setVisibility(View.VISIBLE); break; case GROUP_SURVEYS: divider.setMinimumHeight((int) PlatformUtil.dp2Pixel(getActivity(), 3)); tv.setTextSize(ITEM_TEXT_SIZE); - tv.setTextColor(getResources().getColor(R.color.black_disabled)); + tv.setTextColor(ContextCompat.getColor(getActivity(), R.color.black_disabled)); tv.setText(R.string.surveys); img.setVisibility(View.GONE); dropdown.setVisibility(View.GONE); @@ -471,7 +486,8 @@ public View getGroupView(int groupPosition, boolean isExpanded, View convertView } @Override - public View getChildView(int groupPosition, int childPosition, boolean isLastChild, View convertView, ViewGroup parent) { + public View getChildView(int groupPosition, int childPosition, boolean isLastChild, + View convertView, ViewGroup parent) { View v = convertView; if (v == null) { v = mInflater.inflate(android.R.layout.simple_list_item_1, null); @@ -485,7 +501,9 @@ public View getChildView(int groupPosition, int childPosition, boolean isLastChi switch (groupPosition) { case GROUP_USERS: - User user = isLastChild ? new User(-1, getString(R.string.new_user)) : mUsers.get(childPosition); + User user = isLastChild ? + new User(-1, getString(R.string.new_user)) : + mUsers.get(childPosition); tv.setText(user.getName()); v.setTag(user); break; @@ -493,8 +511,8 @@ public View getChildView(int groupPosition, int childPosition, boolean isLastChi SurveyGroup sg = mSurveys.get(childPosition); tv.setText(sg.getName()); if (sg.getId() == FlowApp.getApp().getSurveyGroupId()) { - tv.setTextColor(getResources().getColorStateList(mHighlightColor)); - v.setBackgroundColor(getResources().getColor(R.color.background_alternate)); + tv.setTextColor(mHighlightColor); + v.setBackgroundColor(ContextCompat.getColor(getActivity(), R.color.background_alternate)); } v.setTag(sg); break; @@ -522,7 +540,8 @@ public boolean onGroupClick(ExpandableListView parent, View v, int groupPosition } @Override - public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { + public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, + int childPosition, long id) { switch (groupPosition) { case GROUP_USERS: User user = (User) v.getTag(); diff --git a/app/src/main/java/org/akvo/flow/ui/fragment/FormListFragment.java b/app/src/main/java/org/akvo/flow/ui/fragment/FormListFragment.java index f0c72c92c..f282fb359 100644 --- a/app/src/main/java/org/akvo/flow/ui/fragment/FormListFragment.java +++ b/app/src/main/java/org/akvo/flow/ui/fragment/FormListFragment.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2013-2014 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2013-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -16,17 +16,17 @@ package org.akvo.flow.ui.fragment; -import java.util.ArrayList; -import java.util.Date; -import java.util.List; - import android.app.Activity; import android.content.Context; import android.database.Cursor; import android.os.Bundle; +import android.support.annotation.NonNull; import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.text.SpannableString; +import android.text.TextUtils; +import android.text.style.AbsoluteSizeSpan; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -45,26 +45,35 @@ import org.akvo.flow.util.PlatformUtil; import org.ocpsoft.prettytime.PrettyTime; +import java.util.ArrayList; +import java.util.Date; +import java.util.List; + +import static android.text.Spanned.SPAN_INCLUSIVE_INCLUSIVE; + public class FormListFragment extends ListFragment implements LoaderCallbacks, OnItemClickListener { private static final String TAG = FormListFragment.class.getSimpleName(); - + private static final String EXTRA_SURVEY_GROUP = "survey_group"; - private static final String EXTRA_RECORD = "record"; - + private static final String EXTRA_RECORD = "record"; + public interface SurveyListListener { void onSurveyClick(String surveyId); } - + private SurveyGroup mSurveyGroup; private SurveyedLocale mRecord; private boolean mRegistered; - + private SurveyAdapter mAdapter; private SurveyDbAdapter mDatabase; private SurveyListListener mListener; - - public static FormListFragment instantiate(SurveyGroup surveyGroup, SurveyedLocale record) { + + public FormListFragment() { + } + + public static FormListFragment newInstance(SurveyGroup surveyGroup, SurveyedLocale record) { FormListFragment fragment = new FormListFragment(); Bundle args = new Bundle(); args.putSerializable(EXTRA_SURVEY_GROUP, surveyGroup); @@ -72,21 +81,21 @@ public static FormListFragment instantiate(SurveyGroup surveyGroup, SurveyedLoca fragment.setArguments(args); return fragment; } - + @Override public void onAttach(Activity activity) { super.onAttach(activity); - + // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { - mListener = (SurveyListListener)activity; + mListener = (SurveyListListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement SurveyListListener"); } } - + @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); @@ -98,7 +107,7 @@ public void onCreate(Bundle savedInstanceState) { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); setHasOptionsMenu(true); - if(mAdapter == null) { + if (mAdapter == null) { mAdapter = new SurveyAdapter(getActivity()); setListAdapter(mAdapter); } @@ -129,13 +138,14 @@ private boolean isRegistrationSurvey(String surveyId) { return surveyId.equals(mSurveyGroup.getRegisterSurveyId()); } + //TODO: make static to avoid memory leaks class SurveyAdapter extends ArrayAdapter { static final int LAYOUT_RES = R.layout.survey_item; public SurveyAdapter(Context context) { super(context, LAYOUT_RES, new ArrayList()); } - + @Override public boolean areAllItemsEnabled() { return false; @@ -143,20 +153,20 @@ public boolean areAllItemsEnabled() { @Override public boolean isEnabled(int position) { - SurveyInfo s = getItem(position); - return !s.mDeleted && isEnabled(s.mId); + SurveyInfo surveyInfo = getItem(position); + return !surveyInfo.mDeleted && isSurveyEnabled(surveyInfo.mId); } - - private boolean isEnabled(String surveyId) { + + private boolean isSurveyEnabled(String surveyId) { if (mSurveyGroup.isMonitored()) { - return isRegistrationSurvey(surveyId) ? !mRegistered : mRegistered; + return isRegistrationSurvey(surveyId) != mRegistered; } - + return !mRegistered;// Not monitored. Only one response allowed } - @Override - public View getView(int position, View convertView, ViewGroup parent) { + @NonNull @Override + public View getView(int position, View convertView, @NonNull ViewGroup parent) { View listItem = convertView; if (listItem == null) { LayoutInflater inflater = LayoutInflater.from(getContext()); @@ -165,23 +175,28 @@ public View getView(int position, View convertView, ViewGroup parent) { final SurveyInfo surveyInfo = getItem(position); - TextView surveyNameView = (TextView)listItem.findViewById(R.id.text1); - TextView surveyVersionView = (TextView)listItem.findViewById(R.id.text2); - TextView lastSubmissionTitle = (TextView)listItem.findViewById(R.id.date_label); - TextView lastSubmissionView = (TextView)listItem.findViewById(R.id.date); + TextView surveyNameView = (TextView) listItem.findViewById(R.id.survey_name_tv); + TextView lastSubmissionTitle = (TextView) listItem.findViewById(R.id.date_label); + TextView lastSubmissionView = (TextView) listItem.findViewById(R.id.date); - surveyNameView.setText(surveyInfo.mName); - surveyVersionView.setText("v" + surveyInfo.mVersion); + StringBuilder surveyExtraInfo = new StringBuilder(20); + String version = surveyInfo == null ? "" : surveyInfo.mVersion; + surveyExtraInfo.append(" v").append(version); - boolean enabled = isEnabled(surveyInfo.mId); + boolean enabled = isSurveyEnabled(surveyInfo.mId); if (surveyInfo.mDeleted) { enabled = false; - surveyVersionView.append(" - " + getString(R.string.form_deleted)); + surveyExtraInfo.append(" - ").append(getString(R.string.form_deleted)); } - + SpannableString versionSpannable = getSpannableString( + getResources().getDimensionPixelSize(R.dimen.survey_version_text_size), + surveyExtraInfo.toString()); + SpannableString titleSpannable = getSpannableString( + getResources().getDimensionPixelSize(R.dimen.survey_title_text_size), + surveyInfo.mName); + surveyNameView.setText(TextUtils.concat(titleSpannable, versionSpannable)); listItem.setEnabled(enabled); surveyNameView.setEnabled(enabled); - surveyVersionView.setEnabled(enabled); if (surveyInfo.mLastSubmission != null && !isRegistrationSurvey(surveyInfo.mId)) { String time = new PrettyTime().format(new Date(surveyInfo.mLastSubmission)); @@ -195,17 +210,26 @@ public View getView(int position, View convertView, ViewGroup parent) { // Alternate background int attr = position % 2 == 0 ? R.attr.listitem_bg1 : R.attr.listitem_bg2; - final int res= PlatformUtil.getResource(getContext(), attr); + final int res = PlatformUtil.getResource(getContext(), attr); listItem.setBackgroundResource(res); return listItem; } - + + @NonNull + private SpannableString getSpannableString(int textSize, String string) { + SpannableString spannable = new SpannableString(string); + spannable.setSpan(new AbsoluteSizeSpan(textSize), 0, string.length(), + SPAN_INCLUSIVE_INCLUSIVE); + return spannable; + } + } @Override public Loader onCreateLoader(int id, Bundle args) { - return new SurveyInfoLoader(getActivity(), mDatabase, mSurveyGroup.getId(), mRecord.getId()); + return new SurveyInfoLoader(getActivity(), mDatabase, mSurveyGroup.getId(), + mRecord.getId()); } @Override @@ -216,7 +240,7 @@ public void onLoadFinished(Loader loader, Cursor cursor) { } mAdapter.clear(); - List surveys = new ArrayList();// Buffer items before adapter addition + List surveys = new ArrayList<>();// Buffer items before adapter addition mRegistered = false; // Calculate if this record is registered yet if (cursor.moveToFirst()) { do { @@ -253,7 +277,7 @@ public void onLoaderReset(Loader loader) { /** * Wrapper for the data displayed in the list */ - class SurveyInfo { + private static class SurveyInfo { String mId; String mName; String mVersion; diff --git a/app/src/main/java/org/akvo/flow/ui/fragment/MapFragment.java b/app/src/main/java/org/akvo/flow/ui/fragment/MapFragment.java index 2009c3808..b7e21e8c0 100644 --- a/app/src/main/java/org/akvo/flow/ui/fragment/MapFragment.java +++ b/app/src/main/java/org/akvo/flow/ui/fragment/MapFragment.java @@ -1,17 +1,16 @@ /* - * Copyright (C) 2013-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.ui.fragment; @@ -22,8 +21,8 @@ import android.location.Criteria; import android.location.Location; import android.location.LocationManager; -import android.os.AsyncTask; import android.os.Bundle; +import android.support.annotation.Nullable; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; import android.text.TextUtils; @@ -33,18 +32,10 @@ import android.view.ViewGroup; import android.widget.FrameLayout; -import org.akvo.flow.R; -import org.akvo.flow.activity.RecordActivity; -import org.akvo.flow.activity.SurveyActivity; -import org.akvo.flow.async.loader.SurveyedLocaleLoader; -import org.akvo.flow.dao.SurveyDbAdapter; -import org.akvo.flow.domain.SurveyGroup; -import org.akvo.flow.domain.SurveyedLocale; -import org.akvo.flow.util.ConstantUtil; - import com.google.android.gms.maps.CameraUpdateFactory; import com.google.android.gms.maps.GoogleMap; import com.google.android.gms.maps.GoogleMap.OnInfoWindowClickListener; +import com.google.android.gms.maps.OnMapReadyCallback; import com.google.android.gms.maps.SupportMapFragment; import com.google.android.gms.maps.model.CameraPosition; import com.google.android.gms.maps.model.LatLng; @@ -55,11 +46,25 @@ import com.google.maps.android.clustering.ClusterManager; import com.google.maps.android.clustering.view.DefaultClusterRenderer; +import org.akvo.flow.R; +import org.akvo.flow.activity.RecordActivity; +import org.akvo.flow.activity.SurveyActivity; +import org.akvo.flow.async.loader.SurveyedLocaleLoader; +import org.akvo.flow.dao.SurveyDbAdapter; +import org.akvo.flow.domain.SurveyGroup; +import org.akvo.flow.domain.SurveyedLocale; +import org.akvo.flow.util.ConstantUtil; + +import java.lang.ref.WeakReference; import java.util.ArrayList; import java.util.List; -public class MapFragment extends SupportMapFragment implements LoaderCallbacks, OnInfoWindowClickListener { +//TODO: separate single data point and multiple into different classes for clarity +public class MapFragment extends SupportMapFragment + implements LoaderCallbacks, OnInfoWindowClickListener, OnMapReadyCallback { + private static final String TAG = MapFragment.class.getSimpleName(); + public static final int MAP_ZOOM_LEVEL = 10; private SurveyGroup mSurveyGroup; private SurveyDbAdapter mDatabase; @@ -69,14 +74,16 @@ public class MapFragment extends SupportMapFragment implements LoaderCallbacks mItems; private boolean mSingleRecord = false; + @Nullable private GoogleMap mMap; + private ClusterManager mClusterManager; - public static MapFragment newInstance(SurveyGroup surveyGroup, String datapointId) { + public static MapFragment newInstance(SurveyGroup surveyGroup, String dataPointId) { MapFragment fragment = new MapFragment(); Bundle args = new Bundle(); args.putSerializable(SurveyActivity.EXTRA_SURVEY_GROUP, surveyGroup); - args.putString(RecordActivity.EXTRA_RECORD_ID, datapointId); + args.putString(RecordActivity.EXTRA_RECORD_ID, dataPointId); fragment.setArguments(args); return fragment; } @@ -86,7 +93,8 @@ public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mItems = new ArrayList<>(); - mSurveyGroup = (SurveyGroup)getArguments().getSerializable(SurveyActivity.EXTRA_SURVEY_GROUP); + mSurveyGroup = (SurveyGroup) getArguments() + .getSerializable(SurveyActivity.EXTRA_SURVEY_GROUP); mRecordId = getArguments().getString(RecordActivity.EXTRA_RECORD_ID); mSingleRecord = !TextUtils.isEmpty(mRecordId);// Single datapoint mode? } @@ -98,10 +106,10 @@ public void onAttach(Activity activity) { // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { - mListener = (RecordListListener)activity; + mListener = (RecordListListener) activity; } catch (ClassCastException e) { - throw new ClassCastException(activity.toString() - + " must implement SurveyedLocalesFragmentListener"); + throw new ClassCastException( + activity.toString() + " must implement SurveyedLocalesFragmentListener"); } } @@ -109,18 +117,22 @@ public void onAttach(Activity activity) { public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); mDatabase = new SurveyDbAdapter(getActivity()); - if (mMap == null) { - mMap = getMap(); - configMap(); - } + getMapAsync(this); + } + + @Override + public void onMapReady(GoogleMap googleMap) { + mMap = googleMap; + configMap(); + refresh(); } private void configMap() { if (mMap != null) { mMap.setMyLocationEnabled(true); mMap.setOnInfoWindowClickListener(this); - mClusterManager = new ClusterManager(getActivity(), mMap); - mClusterManager.setRenderer(new PointRenderer()); + mClusterManager = new ClusterManager<>(getActivity(), mMap); + mClusterManager.setRenderer(new PointRenderer(mMap, getActivity(), mClusterManager)); mMap.setOnMarkerClickListener(mClusterManager); mMap.setOnCameraChangeListener(new GoogleMap.OnCameraChangeListener() { @Override @@ -143,20 +155,30 @@ private void cluster() { double lonDst = Math.abs(ne.longitude - sw.longitude); final double scale = 1d; - LatLngBounds newBounds = bounds - .including(new LatLng(ne.latitude + latDst/scale, ne.longitude + lonDst/scale)) - .including(new LatLng(sw.latitude - latDst/scale, ne.longitude + lonDst/scale)) - .including(new LatLng(sw.latitude - latDst/scale, sw.longitude - lonDst/scale)) - .including(new LatLng(ne.latitude + latDst/scale, sw.longitude - lonDst/scale)); - - new DynamicallyAddMarkerTask().execute(newBounds); + LatLngBounds newBounds = + bounds.including( + new LatLng(ne.latitude + latDst / scale, ne.longitude + lonDst / scale)) + .including(new LatLng(sw.latitude - latDst / scale, + ne.longitude + lonDst / scale)) + .including(new LatLng(sw.latitude - latDst / scale, + sw.longitude - lonDst / scale)) + .including(new LatLng(ne.latitude + latDst / scale, + sw.longitude - lonDst / scale)); + + mClusterManager.clearItems(); + for (SurveyedLocale item : mItems) { + if (item.getPosition() != null && newBounds.contains(item.getPosition())) { + mClusterManager.addItem(item); + } + } + mClusterManager.cluster(); } /** * Center the map in the given record's coordinates. If no record is provided, * the user's location will be used. */ - private void centerMap(SurveyedLocale record) { + private void centerMap(@Nullable SurveyedLocale record) { if (mMap == null) { return; // Not ready yet } @@ -182,7 +204,7 @@ private void centerMap(SurveyedLocale record) { } if (position != null) { - mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, 10)); + mMap.animateCamera(CameraUpdateFactory.newLatLngZoom(position, MAP_ZOOM_LEVEL)); } } @@ -204,7 +226,7 @@ public void onPause() { @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, - Bundle savedInstanceState) { + Bundle savedInstanceState) { View mapView = super.onCreateView(inflater, container, savedInstanceState); View v = inflater.inflate(R.layout.map_fragment, container, false); @@ -228,22 +250,26 @@ public void refresh() { if (isResumed()) { if (mSingleRecord) { // Just get it from the DB - SurveyedLocale record = mDatabase.getSurveyedLocale(mRecordId); - if (mMap != null && record != null && record.getLatitude() != null - && record.getLongitude() != null) { - mMap.clear(); - mMap.addMarker(new MarkerOptions() - .position(new LatLng(record.getLatitude(), record.getLongitude())) - .title(record.getDisplayName(getActivity())) - .snippet(record.getId())); - centerMap(record); - } + updateSingleRecord(); } else { getLoaderManager().restartLoader(0, null, this); } } } + private void updateSingleRecord() { + SurveyedLocale record = mDatabase.getSurveyedLocale(mRecordId); + if (mMap != null && record != null && record.getLatitude() != null + && record.getLongitude() != null) { + mMap.clear(); + mMap.addMarker(new MarkerOptions() + .position(new LatLng(record.getLatitude(), record.getLongitude())) + .title(record.getDisplayName(getActivity())) + .snippet(record.getId())); + centerMap(record); + } + } + @Override public void onInfoWindowClick(Marker marker) { if (mSingleRecord) { @@ -260,7 +286,8 @@ public void onInfoWindowClick(Marker marker) { @Override public Loader onCreateLoader(int id, Bundle args) { long surveyId = mSurveyGroup != null ? mSurveyGroup.getId() : SurveyGroup.ID_NONE; - return new SurveyedLocaleLoader(getActivity(), mDatabase, surveyId, ConstantUtil.ORDER_BY_NONE); + return new SurveyedLocaleLoader(getActivity(), mDatabase, surveyId, + ConstantUtil.ORDER_BY_NONE); } @Override @@ -274,7 +301,6 @@ public void onLoadFinished(Loader loader, Cursor cursor) { mItems.clear(); do { SurveyedLocale item = SurveyDbAdapter.getSurveyedLocale(cursor); - //mClusterManager.addItem(item); mItems.add(item); } while (cursor.moveToNext()); } @@ -290,18 +316,23 @@ public void onLoaderReset(Loader loader) { * This custom renderer overrides original 'bucketed' names, in order to display the accurate * number of markers within a cluster. */ - class PointRenderer extends DefaultClusterRenderer { + private static class PointRenderer extends DefaultClusterRenderer { + + private final WeakReference activityContextWeakRef; - public PointRenderer() { - super(getActivity(), getMap(), mClusterManager); + public PointRenderer(GoogleMap map, Context context, + ClusterManager clusterManager) { + super(context, map, clusterManager); + this.activityContextWeakRef = new WeakReference<>(context); } @Override - protected void onBeforeClusterItemRendered(SurveyedLocale item, MarkerOptions markerOptions) { - markerOptions - .title(item.getDisplayName(getActivity())) - .snippet(item.getId()); - super.onBeforeClusterItemRendered(item, markerOptions); + protected void onBeforeClusterItemRendered(SurveyedLocale item, + MarkerOptions markerOptions) { + Context context = activityContextWeakRef.get(); + if (context != null) { + markerOptions.title(item.getDisplayName(context)).snippet(item.getId()); + } } @Override @@ -313,26 +344,5 @@ protected int getBucket(Cluster cluster) { protected String getClusterText(int bucket) { return String.valueOf(bucket); } - - } - - private class DynamicallyAddMarkerTask extends AsyncTask { - - @Override - protected Void doInBackground(LatLngBounds... bounds) { - mClusterManager.clearItems(); - for (SurveyedLocale item : mItems) { - if (item.getPosition() != null && bounds[0].contains(item.getPosition())) { - mClusterManager.addItem(item); - } - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - mClusterManager.cluster(); - } } - } diff --git a/app/src/main/java/org/akvo/flow/ui/fragment/ResponseListFragment.java b/app/src/main/java/org/akvo/flow/ui/fragment/ResponseListFragment.java index 25a3ed93e..b9aeeb129 100644 --- a/app/src/main/java/org/akvo/flow/ui/fragment/ResponseListFragment.java +++ b/app/src/main/java/org/akvo/flow/ui/fragment/ResponseListFragment.java @@ -27,6 +27,7 @@ import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; @@ -49,12 +50,12 @@ public class ResponseListFragment extends ListFragment implements LoaderCallback private static final String TAG = ResponseListFragment.class.getSimpleName(); private static final String EXTRA_SURVEY_GROUP = "survey_group"; - private static final String EXTRA_RECORD = "record"; + private static final String EXTRA_RECORD = "record"; // TODO: Move all id constants to ConstantUtil - private static int SURVEY_ID_KEY = R.integer.surveyidkey; + private static int SURVEY_ID_KEY = R.integer.surveyidkey; private static int SURVEY_INSTANCE_ID_KEY = R.integer.respidkey; - private static int FINISHED_KEY = R.integer.finishedkey; + private static int FINISHED_KEY = R.integer.finishedkey; // Context menu items private static final int DELETE_ONE = 0; @@ -86,14 +87,14 @@ public void onCreate(Bundle savedInstanceState) { public void onResume() { super.onResume(); refresh(); - getActivity().registerReceiver(dataSyncReceiver, - new IntentFilter(getString(R.string.action_data_sync))); + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(dataSyncReceiver, + new IntentFilter(ConstantUtil.ACTION_DATA_SYNC)); } @Override public void onPause() { super.onPause(); - getActivity().unregisterReceiver(dataSyncReceiver); + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(dataSyncReceiver); } @Override @@ -116,7 +117,7 @@ public void onActivityCreated(Bundle savedInstanceState) { mDatabase.open(); } - if(mAdapter == null) { + if (mAdapter == null) { mAdapter = new ResponseListAdapter(getActivity());// Cursor Adapter setListAdapter(mAdapter); } @@ -131,17 +132,19 @@ public void onCreateContextMenu(ContextMenu menu, View view, menu.add(0, VIEW_HISTORY, 0, R.string.transmissionhist); // Allow deletion only for 'saved' responses - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)menuInfo; + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) menuInfo; View itemView = info.targetView; - if (!(Boolean)itemView.getTag(FINISHED_KEY)) { + if (!(Boolean) itemView.getTag(FINISHED_KEY)) { menu.add(0, DELETE_ONE, 2, R.string.deleteresponse); } } @Override public boolean onContextItemSelected(MenuItem item) { - AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo)item.getMenuInfo(); - Long surveyInstanceId = mAdapter.getItemId(info.position);// This ID is the _id column in the SQLite db + AdapterView.AdapterContextMenuInfo info = (AdapterView.AdapterContextMenuInfo) item + .getMenuInfo(); + Long surveyInstanceId = mAdapter + .getItemId(info.position);// This ID is the _id column in the SQLite db switch (item.getItemId()) { case DELETE_ONE: deleteSurveyInstance(surveyInstanceId); @@ -199,7 +202,7 @@ public void onListItemClick(ListView list, View view, int position, long id) { i.putExtra(ConstantUtil.SURVEYED_LOCALE_ID, mRecord.getId()); // Read-only vs editable - if ((Boolean)view.getTag(FINISHED_KEY)) { + if ((Boolean) view.getTag(FINISHED_KEY)) { i.putExtra(ConstantUtil.READONLY_KEY, true); } else { i.putExtra(ConstantUtil.SINGLE_SURVEY_KEY, true); diff --git a/app/src/main/java/org/akvo/flow/ui/fragment/SurveyedLocaleListFragment.java b/app/src/main/java/org/akvo/flow/ui/fragment/SurveyedLocaleListFragment.java index 76d3572f3..89ee736d5 100644 --- a/app/src/main/java/org/akvo/flow/ui/fragment/SurveyedLocaleListFragment.java +++ b/app/src/main/java/org/akvo/flow/ui/fragment/SurveyedLocaleListFragment.java @@ -16,14 +16,6 @@ package org.akvo.flow.ui.fragment; -import java.util.Date; - -import org.akvo.flow.activity.SurveyActivity; -import org.akvo.flow.domain.SurveyGroup; -import org.akvo.flow.util.GeoUtil; -import org.akvo.flow.util.PlatformUtil; -import org.ocpsoft.prettytime.PrettyTime; - import android.app.Activity; import android.content.BroadcastReceiver; import android.content.Context; @@ -39,6 +31,7 @@ import android.support.v4.app.ListFragment; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; +import android.support.v4.content.LocalBroadcastManager; import android.support.v4.widget.CursorAdapter; import android.text.TextUtils; import android.util.Log; @@ -53,17 +46,25 @@ import android.widget.Toast; import org.akvo.flow.R; +import org.akvo.flow.activity.SurveyActivity; import org.akvo.flow.async.loader.SurveyedLocaleLoader; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.dao.SurveyDbAdapter.RecordColumns; import org.akvo.flow.dao.SurveyDbAdapter.SurveyInstanceColumns; import org.akvo.flow.dao.SurveyDbAdapter.SurveyInstanceStatus; +import org.akvo.flow.domain.SurveyGroup; import org.akvo.flow.domain.SurveyedLocale; import org.akvo.flow.ui.fragment.OrderByDialogFragment.OrderByDialogListener; import org.akvo.flow.util.ConstantUtil; +import org.akvo.flow.util.GeoUtil; +import org.akvo.flow.util.PlatformUtil; +import org.ocpsoft.prettytime.PrettyTime; + +import java.lang.ref.WeakReference; +import java.util.Date; -public class SurveyedLocaleListFragment extends ListFragment implements LocationListener, - OnItemClickListener, LoaderCallbacks, OrderByDialogListener { +public class SurveyedLocaleListFragment extends ListFragment implements LocationListener, + OnItemClickListener, LoaderCallbacks, OrderByDialogListener { private static final String TAG = SurveyedLocaleListFragment.class.getSimpleName(); private LocationManager mLocationManager; @@ -73,7 +74,7 @@ public class SurveyedLocaleListFragment extends ListFragment implements Location private int mOrderBy; private SurveyGroup mSurveyGroup; private SurveyDbAdapter mDatabase; - + private SurveyedLocaleListAdapter mAdapter; private RecordListListener mListener; @@ -88,32 +89,35 @@ public static SurveyedLocaleListFragment newInstance(SurveyGroup surveyGroup) { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); - mSurveyGroup = (SurveyGroup)getArguments().getSerializable(SurveyActivity.EXTRA_SURVEY_GROUP); + mSurveyGroup = (SurveyGroup) getArguments() + .getSerializable(SurveyActivity.EXTRA_SURVEY_GROUP); mOrderBy = ConstantUtil.ORDER_BY_DATE;// Default case setHasOptionsMenu(true); } - + @Override public void onAttach(Activity activity) { super.onAttach(activity); - + // This makes sure that the container activity has implemented // the callback interface. If not, it throws an exception try { - mListener = (RecordListListener)activity; + mListener = (RecordListListener) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement SurveyedLocalesFragmentListener"); } } - + @Override public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - mLocationManager = (LocationManager) getActivity().getSystemService(Context.LOCATION_SERVICE); + mLocationManager = (LocationManager) getActivity() + .getSystemService(Context.LOCATION_SERVICE); mDatabase = new SurveyDbAdapter(getActivity()); - if(mAdapter == null) { - mAdapter = new SurveyedLocaleListAdapter(getActivity()); + if (mAdapter == null) { + mAdapter = new SurveyedLocaleListAdapter(getActivity(), mLatitude, mLongitude, + mSurveyGroup); setListAdapter(mAdapter); } setEmptyText(getString(R.string.no_records_text)); @@ -139,16 +143,16 @@ public void onResume() { } // Listen for data sync updates, so we can update the UI accordingly - getActivity().registerReceiver(dataSyncReceiver, - new IntentFilter(getString(R.string.action_data_sync))); + LocalBroadcastManager.getInstance(getActivity()).registerReceiver(dataSyncReceiver, + new IntentFilter(ConstantUtil.ACTION_DATA_SYNC)); refresh(); } - + @Override public void onPause() { super.onPause(); - getActivity().unregisterReceiver(dataSyncReceiver); + LocalBroadcastManager.getInstance(getActivity()).unregisterReceiver(dataSyncReceiver); mLocationManager.removeUpdates(this); mDatabase.close(); } @@ -162,7 +166,7 @@ public void refresh(SurveyGroup surveyGroup) { * Ideally, we should build a ContentProvider, so this notifications are handled * automatically, and the loaders restarted without this explicit dependency. */ - public void refresh() { + private void refresh() { if (!isResumed()) { return; } @@ -175,7 +179,7 @@ public void refresh() { if (mOrderBy == ConstantUtil.ORDER_BY_DISTANCE && mLatitude == 0.0d && mLongitude == 0.0d) { // Warn user that the location is unknown - Toast.makeText(getActivity(), "Unknown Location", Toast.LENGTH_SHORT).show(); + Toast.makeText(getActivity(), R.string.locale_list_error_unknown_location, Toast.LENGTH_SHORT).show(); return; } getLoaderManager().restartLoader(0, null, this); @@ -186,10 +190,10 @@ public void onItemClick(AdapterView parent, View view, int position, long id) Cursor cursor = (Cursor) mAdapter.getItem(position); final String localeId = cursor.getString(cursor.getColumnIndexOrThrow( RecordColumns.RECORD_ID)); - + mListener.onRecordSelected(localeId);// Notify the host activity } - + @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle item selection @@ -200,7 +204,7 @@ public boolean onOptionsItemSelected(MenuItem item) { dialogFragment.show(getFragmentManager(), "order_by"); return true; } - + return false; } @@ -219,7 +223,8 @@ public void onOrderByClick(int order) { @Override public Loader onCreateLoader(int id, Bundle args) { long surveyId = mSurveyGroup != null ? mSurveyGroup.getId() : SurveyGroup.ID_NONE; - return new SurveyedLocaleLoader(getActivity(), mDatabase, surveyId, mLatitude, mLongitude, mOrderBy); + return new SurveyedLocaleLoader(getActivity(), mDatabase, surveyId, mLatitude, mLongitude, + mOrderBy); } @Override @@ -228,7 +233,7 @@ public void onLoadFinished(Loader loader, Cursor cursor) { Log.e(TAG, "onFinished() - Loader returned no data"); return; } - + mAdapter.swapCursor(cursor); } @@ -236,7 +241,7 @@ public void onLoadFinished(Loader loader, Cursor cursor) { public void onLoaderReset(Loader loader) { mAdapter.swapCursor(null); } - + // ==================================== // // ======== Location Callbacks ======== // // ==================================== // @@ -266,27 +271,29 @@ public void onStatusChanged(String provider, int status, Bundle extras) { * BroadcastReceiver to notify of data synchronisation. This should be * fired from DataSyncService. */ - private BroadcastReceiver dataSyncReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - Log.i(TAG, "Survey Instance status has changed. Refreshing UI..."); - refresh(); - } - }; + private final BroadcastReceiver dataSyncReceiver = new DataSyncBroadcastReceiver(this); /** * List Adapter to bind the Surveyed Locales into the list items */ - class SurveyedLocaleListAdapter extends CursorAdapter { + private static class SurveyedLocaleListAdapter extends CursorAdapter { + + private final double mLatitude; + private final double mLongitude; + private final SurveyGroup mSurveyGroup; - public SurveyedLocaleListAdapter(Context context) { + public SurveyedLocaleListAdapter(Context context, double mLatitude, double mLongitude, + SurveyGroup mSurveyGroup) { super(context, null, false); + this.mLatitude = mLatitude; + this.mLongitude = mLongitude; + this.mSurveyGroup = mSurveyGroup; } @Override public View newView(Context context, Cursor c, ViewGroup parent) { - LayoutInflater inflater = LayoutInflater.from(getActivity()); - return inflater.inflate(R.layout.surveyed_locale_item, null); + LayoutInflater inflater = LayoutInflater.from(parent.getContext()); + return inflater.inflate(R.layout.surveyed_locale_item, parent, false); } @Override @@ -301,11 +308,10 @@ public void bindView(View view, Context context, Cursor c) { // This cursor contains extra info about the Record status int status = c.getInt(c.getColumnIndexOrThrow(SurveyInstanceColumns.STATUS)); - nameView.setText(surveyedLocale.getDisplayName(context)); idView.setText(surveyedLocale.getId()); - displayDistanceText(distanceView, getDistanceText(surveyedLocale)); + displayDistanceText(distanceView, getDistanceText(surveyedLocale, context)); displayDateText(dateView, surveyedLocale.getLastModified()); int statusRes = 0; @@ -313,17 +319,20 @@ public void bindView(View view, Context context, Cursor c) { switch (status) { case SurveyInstanceStatus.SAVED: statusRes = R.drawable.record_saved_icn; - statusText = getString(R.string.status_saved); + statusText = context.getString(R.string.status_saved); break; case SurveyInstanceStatus.SUBMITTED: case SurveyInstanceStatus.EXPORTED: statusRes = R.drawable.record_exported_icn; - statusText = getString(R.string.status_exported); + statusText = context.getString(R.string.status_exported); break; case SurveyInstanceStatus.SYNCED: case SurveyInstanceStatus.DOWNLOADED: statusRes = R.drawable.record_synced_icn; - statusText = getString(R.string.status_synced); + statusText = context.getString(R.string.status_synced); + break; + default: + //wrong state break; } @@ -332,17 +341,19 @@ public void bindView(View view, Context context, Cursor c) { // Alternate background int attr = c.getPosition() % 2 == 0 ? R.attr.listitem_bg1 : R.attr.listitem_bg2; - final int res= PlatformUtil.getResource(context, attr); + final int res = PlatformUtil.getResource(context, attr); view.setBackgroundResource(res); } - private String getDistanceText(SurveyedLocale surveyedLocale) { - StringBuilder builder = new StringBuilder(getString(R.string.distance_label) + " "); - + private String getDistanceText(SurveyedLocale surveyedLocale, Context context) { + StringBuilder builder = new StringBuilder( + context.getString(R.string.distance_label) + " "); + if (surveyedLocale.getLatitude() != null && surveyedLocale.getLongitude() != null && (mLatitude != 0.0d || mLongitude != 0.0d)) { float[] results = new float[1]; - Location.distanceBetween(mLatitude, mLongitude, surveyedLocale.getLatitude(), surveyedLocale.getLongitude(), results); + Location.distanceBetween(mLatitude, mLongitude, surveyedLocale.getLatitude(), + surveyedLocale.getLongitude(), results); final double distance = results[0]; builder.append(GeoUtil.getDisplayLength(distance)); @@ -359,7 +370,8 @@ private void displayDateText(TextView tv, Long time) { if (mSurveyGroup != null && mSurveyGroup.isMonitored()) { labelRes = R.string.last_modified_monitored; } - tv.setText(getString(labelRes) + " " + new PrettyTime().format(new Date(time))); + tv.setText(tv.getContext().getString(labelRes) + " " + new PrettyTime() + .format(new Date(time))); } else { tv.setVisibility(View.GONE); } @@ -375,5 +387,22 @@ private void displayDistanceText(TextView tv, String distance) { } } - + + private static class DataSyncBroadcastReceiver extends BroadcastReceiver { + + private final WeakReference fragmentWeakRef; + + private DataSyncBroadcastReceiver(SurveyedLocaleListFragment fragment) { + this.fragmentWeakRef = new WeakReference<>(fragment); + } + + @Override + public void onReceive(Context context, Intent intent) { + Log.i(TAG, "Survey Instance status has changed. Refreshing UI..."); + SurveyedLocaleListFragment fragment = fragmentWeakRef.get(); + if (fragment != null) { + fragment.refresh(); + } + } + } } diff --git a/app/src/main/java/org/akvo/flow/ui/view/CascadeQuestionView.java b/app/src/main/java/org/akvo/flow/ui/view/CascadeQuestionView.java index 81c48d0ad..b310221d1 100644 --- a/app/src/main/java/org/akvo/flow/ui/view/CascadeQuestionView.java +++ b/app/src/main/java/org/akvo/flow/ui/view/CascadeQuestionView.java @@ -48,7 +48,8 @@ import java.util.ArrayList; import java.util.List; -public class CascadeQuestionView extends QuestionView implements AdapterView.OnItemSelectedListener { +public class CascadeQuestionView extends QuestionView + implements AdapterView.OnItemSelectedListener { private static final String TAG = CascadeQuestionView.class.getSimpleName(); private static final int POSITION_NONE = -1;// no spinner position id @@ -69,13 +70,13 @@ public CascadeQuestionView(Context context, Question q, SurveyListener surveyLis private void init() { setQuestionView(R.layout.cascade_question_view); - mSpinnerContainer = (LinearLayout)findViewById(R.id.cascade_content); + mSpinnerContainer = (LinearLayout) findViewById(R.id.cascade_content); // Load level names List levels = getQuestion().getLevels(); if (levels != null) { mLevels = new String[levels.size()]; - for (int i=0; i values, int selection) { LayoutInflater inflater = LayoutInflater.from(getContext()); View view = inflater.inflate(R.layout.cascading_level_item, mSpinnerContainer, false); - final TextView text = (TextView)view.findViewById(R.id.text); - final Spinner spinner = (Spinner)view.findViewById(R.id.spinner); + final TextView text = (TextView) view.findViewById(R.id.text); + final Spinner spinner = (Spinner) view.findViewById(R.id.spinner); text.setText(mLevels != null && mLevels.length > position ? mLevels[position] : ""); @@ -174,7 +175,7 @@ public void run() { @Override public void onItemSelected(AdapterView parent, View view, int position, long id) { - final int index = (Integer)parent.getTag(); + final int index = (Integer) parent.getTag(); updateSpinners(index); captureResponse(); setError(null); @@ -204,7 +205,7 @@ public void rehydrate(QuestionResponse resp) { while (index < values.size()) { int valuePosition = POSITION_NONE; List spinnerValues = mDatabase.getValues(parentId); - for (int pos=0; pos values = new ArrayList<>(); - for (int i=0; i { + private static class CascadeAdapter extends ArrayAdapter { CascadeAdapter(Context context, List objects) { - super(context, android.R.layout.simple_spinner_item, objects); - setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); + super(context, R.layout.cascade_spinner_item, R.id.cascade_spinner_item_text, objects); + setDropDownViewResource(R.layout.cascade_spinner_item); } @Override - public View getView (int position, View convertView, ViewGroup parent) { + public View getView(int position, View convertView, ViewGroup parent) { View view = super.getView(position, convertView, parent); setStyle(view, position); return view; } @Override - public View getDropDownView (int position, View convertView, ViewGroup parent) { + public View getDropDownView(final int position, View convertView, ViewGroup parent) { View view = super.getDropDownView(position, convertView, parent); setStyle(view, position); return view; @@ -292,7 +293,7 @@ public View getDropDownView (int position, View convertView, ViewGroup parent) { private void setStyle(View view, int position) { try { - TextView text = (TextView)view; + TextView text = (TextView) view.findViewById(R.id.cascade_spinner_item_text); int flags = text.getPaintFlags(); if (position == 0) { flags |= Paint.FAKE_BOLD_TEXT_FLAG; diff --git a/app/src/main/java/org/akvo/flow/ui/view/QuestionGroupTab.java b/app/src/main/java/org/akvo/flow/ui/view/QuestionGroupTab.java index e4bfccb5e..f04b4a8bd 100644 --- a/app/src/main/java/org/akvo/flow/ui/view/QuestionGroupTab.java +++ b/app/src/main/java/org/akvo/flow/ui/view/QuestionGroupTab.java @@ -13,6 +13,7 @@ * * The full license text can also be seen at . */ + package org.akvo.flow.ui.view; import android.animation.LayoutTransition; @@ -43,6 +44,7 @@ import java.util.Set; public class QuestionGroupTab extends LinearLayout implements RepetitionHeader.OnDeleteListener { + private QuestionGroup mQuestionGroup; private QuestionInteractionListener mQuestionListener; private SurveyListener mSurveyListener; @@ -82,9 +84,9 @@ private void init() { setFocusableInTouchMode(true); inflate(getContext(), R.layout.question_group_tab, this); - mScroller = (ScrollView)findViewById(R.id.scroller); - mContainer = (LinearLayout)findViewById(R.id.question_list); - mRepetitionsText = (TextView)findViewById(R.id.repeat_header); + mScroller = (ScrollView) findViewById(R.id.scroller); + mContainer = (LinearLayout) findViewById(R.id.question_list); + mRepetitionsText = (TextView) findViewById(R.id.repeat_header); // Animate view additions/removals if possible if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.HONEYCOMB) { @@ -166,11 +168,15 @@ public void loadState() { // Load existing iterations. If no iteration is available, show one by default. mRepetitions.loadIDs(); int iterCount = Math.max(mRepetitions.size(), 1); - for (int i=0; i responses = mSurveyListener.getResponses(); for (QuestionView qv : mQuestionViews.values()) { final String questionId = qv.getQuestion().getId(); @@ -223,7 +229,8 @@ public boolean isLoaded() { } private void updateRepetitionsHeader() { - mRepetitionsText.setText(getContext().getString(R.string.repetitions) + mRepetitions.size()); + mRepetitionsText + .setText(getContext().getString(R.string.repetitions) + mRepetitions.size()); } private void loadGroup() { @@ -231,13 +238,18 @@ private void loadGroup() { } private void loadGroup(int index) { - final int repetitionId = mRepetitions.size() <= index ? mRepetitions.next() : mRepetitions.getRepetitionId(index); - final int position = index+1;// Visual indicator. + final int repetitionId = + mRepetitions.size() <= index ? + mRepetitions.next() : + mRepetitions.getRepetitionId(index); + final int position = index + 1;// Visual indicator. if (mQuestionGroup.isRepeatable()) { updateRepetitionsHeader(); - RepetitionHeader header = new RepetitionHeader(getContext(), mQuestionGroup.getHeading(), - repetitionId, position, mSurveyListener.isReadOnly() ? null : this); + RepetitionHeader header = + new RepetitionHeader(getContext(), mQuestionGroup.getHeading(), repetitionId, + position, + mSurveyListener.isReadOnly() ? null : this); mHeaders.put(repetitionId, header); mContainer.addView(header); } @@ -245,7 +257,8 @@ private void loadGroup(int index) { final Context context = getContext(); for (Question q : mQuestionGroup.getQuestions()) { if (mQuestionGroup.isRepeatable()) { - q = Question.copy(q, q.getId() + "|" + repetitionId);// compound id. (qid|repetition) + q = Question + .copy(q, q.getId() + "|" + repetitionId);// compound id. (qid|repetition) } QuestionView questionView; @@ -359,6 +372,7 @@ private int parseRepetitionId(String questionId) { } class Repetitions implements Iterable { + List mIDs = new ArrayList<>(); /** @@ -409,5 +423,4 @@ public Iterator iterator() { return mIDs.iterator(); } } - } diff --git a/app/src/main/java/org/akvo/flow/ui/view/QuestionView.java b/app/src/main/java/org/akvo/flow/ui/view/QuestionView.java index d843808b9..170ddabe0 100644 --- a/app/src/main/java/org/akvo/flow/ui/view/QuestionView.java +++ b/app/src/main/java/org/akvo/flow/ui/view/QuestionView.java @@ -16,11 +16,6 @@ package org.akvo.flow.ui.view; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.StringTokenizer; - import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; @@ -48,6 +43,11 @@ import org.akvo.flow.util.PlatformUtil; import org.akvo.flow.util.ViewUtil; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + public abstract class QuestionView extends LinearLayout implements QuestionInteractionListener { private static final int PADDING_DIP = 8; protected static String[] sColors = null; @@ -70,7 +70,7 @@ public abstract class QuestionView extends LinearLayout implements QuestionInter public QuestionView(final Context context, Question q, SurveyListener surveyListener) { super(context); setOrientation(VERTICAL); - final int padding = (int)PlatformUtil.dp2Pixel(getContext(), PADDING_DIP); + final int padding = (int) PlatformUtil.dp2Pixel(getContext(), PADDING_DIP); setPadding(padding, padding, padding, padding); if (sColors == null) { // must have enough colors for all enabled languages @@ -84,7 +84,6 @@ public QuestionView(final Context context, Question q, SurveyListener surveyList /** * Inflate the appropriate layout file, and retrieve the references to the common resources. * Subclasses' layout files should ALWAYS contain the question_header view. - * * Inflated layout will be attached to the View's root, thus all the elements within it * will be accessible by calling findViewById(int) * @@ -94,8 +93,8 @@ protected void setQuestionView(int layoutRes) { LayoutInflater inflater = LayoutInflater.from(getContext()); inflater.inflate(layoutRes, this, true); - mQuestionText = (TextView)findViewById(R.id.question_tv); - mTipImage = (ImageButton)findViewById(R.id.tip_ib); + mQuestionText = (TextView) findViewById(R.id.question_tv); + mTipImage = (ImageButton) findViewById(R.id.tip_ib); if (mQuestionText == null || mTipImage == null) { throw new RuntimeException( @@ -169,7 +168,7 @@ private Spanned formText() { boolean isFirst = true; StringBuilder text = new StringBuilder(); if (mQuestion.isMandatory()) { - text.append(""); + text.append(""); } text.append(mQuestion.getOrder()).append(". ");// Prefix the text with the order @@ -198,7 +197,7 @@ private Spanned formText() { } } if (mQuestion.isMandatory()) { - text = text.append("*"); + text = text.append("*"); } return Html.fromHtml(text.toString()); } diff --git a/app/src/main/java/org/akvo/flow/ui/view/RepetitionHeader.java b/app/src/main/java/org/akvo/flow/ui/view/RepetitionHeader.java index c0844d586..bf4300c89 100644 --- a/app/src/main/java/org/akvo/flow/ui/view/RepetitionHeader.java +++ b/app/src/main/java/org/akvo/flow/ui/view/RepetitionHeader.java @@ -18,6 +18,7 @@ import android.content.Context; import android.content.DialogInterface; import android.graphics.drawable.Drawable; +import android.support.v4.content.ContextCompat; import android.util.TypedValue; import android.view.MotionEvent; import android.view.View; @@ -48,12 +49,12 @@ public RepetitionHeader(Context context, String title, int id, int pos, OnDelete setPadding(padding, padding, padding, padding); setTextSize(TypedValue.COMPLEX_UNIT_SP, 20); setText(mTitle + " - " + pos); - setTextColor(getResources().getColor(R.color.text_color_secondary)); - setBackgroundColor(getResources().getColor(R.color.background_alternate)); + setTextColor(ContextCompat.getColor(context, R.color.repetitions_text_color)); + setBackgroundColor(ContextCompat.getColor(context, R.color.background_alternate)); // Show 'delete' icon if the OnDeleteListener param is not null if (mListener != null) { - Drawable deleteIcon = getContext().getResources().getDrawable(R.drawable.ic_trash); + Drawable deleteIcon = ContextCompat.getDrawable(context, R.drawable.ic_trash); setCompoundDrawablesWithIntrinsicBounds(null, null, deleteIcon, null); setOnTouchListener(this); } diff --git a/app/src/main/java/org/akvo/flow/util/ConstantUtil.java b/app/src/main/java/org/akvo/flow/util/ConstantUtil.java index b04a6f63e..f65708c68 100644 --- a/app/src/main/java/org/akvo/flow/util/ConstantUtil.java +++ b/app/src/main/java/org/akvo/flow/util/ConstantUtil.java @@ -1,24 +1,23 @@ /* - * Copyright (C) 2010-2015 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2017 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.util; /** * Class to hold all public constants used in the application - * + * * @author Christopher Fagiani */ public class ConstantUtil { @@ -128,10 +127,7 @@ public class ConstantUtil { */ public static final String USER_ID_KEY = "UID"; public static final String SURVEY_ID_KEY = "SID"; - public static final String ID_KEY = "_id"; public static final String RESPONDENT_ID_KEY = "survey_respondent_id"; - public static final String IMAGE_URL_LIST_KEY = "imageurls"; - public static final String IMAGE_CAPTION_LIST_KEY = "imagecaps"; public static final String READONLY_KEY = "readonly"; public static final String SINGLE_SURVEY_KEY = "single_survey"; public static final String SURVEY_GROUP = "survey_group"; @@ -143,7 +139,6 @@ public class ConstantUtil { public static final String SURVEY_LANG_SETTING_KEY = "survey.language"; public static final String SURVEY_LANG_PRESENT_KEY = "survey.languagespresent"; public static final String CELL_UPLOAD_SETTING_KEY = "data.cellular.upload"; - public static final String LOCATION_BEACON_SETTING_KEY = "location.sendbeacon"; public static final String SERVER_SETTING_KEY = "backend.server"; public static final String SCREEN_ON_KEY = "screen.keepon"; public static final String DEVICE_IDENT_KEY = "device.identifier"; @@ -167,12 +162,6 @@ public class ConstantUtil { */ public static final String ENGLISH_CODE = "en"; - /** - * html colors - */ - public static final String WHITE_COLOR = "white"; - public static final String BLACK_COLOR = "black"; - /** * "code" to prevent unauthorized use of administrative settings/preferences */ @@ -199,31 +188,31 @@ public class ConstantUtil { */ public static final String RESOURCE_PACKAGE = "org.akvo.flow"; public static final String RAW_RESOURCE = "raw"; - + /** * SurveyedLocale meta question IDs. Negative IDs to avoid collisions. - * Irrelevant for the server side, they are used to identify a locale meta-data + * Irrelevant for the server side, they are used to identify a locale meta-data * response among the rest of the 'real' question answers */ public static final String QUESTION_LOCALE_NAME = "-1"; public static final String QUESTION_LOCALE_GEO = "-2"; - + /** * Order By */ - public static final int ORDER_BY_NONE = -1; - public static final int ORDER_BY_DATE = 0; + public static final int ORDER_BY_NONE = -1; + public static final int ORDER_BY_DATE = 0; public static final int ORDER_BY_DISTANCE = 1; - public static final int ORDER_BY_STATUS = 2; - public static final int ORDER_BY_NAME = 3; + public static final int ORDER_BY_STATUS = 2; + public static final int ORDER_BY_NAME = 3; /** * Max picture size * Values must match the ones set in arrays. * TODO: Preferences should be managed with SharedPreferences api, to avoid this error prone references */ - public static final int IMAGE_SIZE_320_240 = 0; - public static final int IMAGE_SIZE_640_480 = 1; + public static final int IMAGE_SIZE_320_240 = 0; + public static final int IMAGE_SIZE_640_480 = 1; public static final int IMAGE_SIZE_1280_960 = 2; public static final int NOTIFICATION_RECORD_SYNC = 100; @@ -252,6 +241,26 @@ public class ConstantUtil { public static final String CADDISFLY_IMAGE = "image"; public static final String CADDISFLY_MIME = "text/plain"; + //broadcasts + public static final String ACTION_LOCALE_SYNC = "fieldsurvey.ACTION_LOCALES_SYNC"; + public static final String ACTION_DATA_SYNC = "fieldsurvey.ACTION_DATA_SYNC"; + + //apk update + public static final int REPEAT_INTERVAL_IN_SECONDS = 1 * 60 * 60 * 24; //every 24Hrs + public static final int FLEX_INTERVAL_IN_SECONDS = 1 * 60 * 60; //1 hour + + //first runs will be faster + public static final int FIRST_REPEAT_INTERVAL_IN_SECONDS = 1 * 60; + public static final int FIRST_FLEX_INTERVAL_IN_SECOND = 30; + + /** + * 7 days + */ + public static final int UPDATE_NOTIFICATION_DELAY_IN_MS = 7 * 60 * 60 * 24 * 1000; + + //requests + public static final int REQUEST_ADD_USER = 0; + /** * prevent instantiation */ diff --git a/app/src/main/java/org/akvo/flow/util/FileUtil.java b/app/src/main/java/org/akvo/flow/util/FileUtil.java index 2853f0fb2..f8ac9baf6 100644 --- a/app/src/main/java/org/akvo/flow/util/FileUtil.java +++ b/app/src/main/java/org/akvo/flow/util/FileUtil.java @@ -16,6 +16,17 @@ package org.akvo.flow.util; +import android.content.Context; +import android.database.Cursor; +import android.media.ExifInterface; +import android.os.Environment; +import android.provider.MediaStore; +import android.text.TextUtils; +import android.util.Log; + +import org.akvo.flow.BuildConfig; +import org.akvo.flow.app.FlowApp; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -34,19 +45,9 @@ import java.util.zip.ZipEntry; import java.util.zip.ZipInputStream; -import android.content.Context; -import android.database.Cursor; -import android.media.ExifInterface; -import android.os.Environment; -import android.provider.MediaStore; -import android.text.TextUtils; -import android.util.Log; - -import org.akvo.flow.app.FlowApp; - /** * utility for manipulating files - * + * * @author Christopher Fagiani */ public class FileUtil { @@ -66,12 +67,13 @@ public class FileUtil { private static final int BUFFER_SIZE = 2048; - public enum FileType {DATA, MEDIA, INBOX, FORMS, STACKTRACE, TMP, APK, RES}; + public enum FileType {DATA, MEDIA, INBOX, FORMS, STACKTRACE, TMP, APK, RES} /** * Get the appropriate files directory for the given FileType. The directory may or may * not be in the app-specific External Storage. The caller cannot assume anything about * the location. + * * @param type FileType to determine the type of resource attempting to use. * @return File representing the root directory for the given FileType. */ @@ -113,10 +115,11 @@ public static File getFilesDir(FileType type) { /** * Get the root of the files storage directory, depending on the resource being app internal * (not concerning the user) or not (users might need to pull the resource from the storage). + * * @param internal true for app specific resources, false otherwise * @return The root directory for this kind of resources */ - private static final String getFilesStorageDir(boolean internal) { + private static String getFilesStorageDir(boolean internal) { if (internal) { return FlowApp.getApp().getExternalFilesDir(null).getAbsolutePath(); } @@ -142,7 +145,7 @@ public static void writeStringToFile(String contents, public static String readFileAsString(File file) throws IOException { StringBuilder contents = new StringBuilder(); BufferedReader input = new BufferedReader(new FileReader(file)); - String line = null; + String line; try { while ((line = input.readLine()) != null) { contents.append(line); @@ -178,7 +181,7 @@ public static void copy(InputStream in, OutputStream out) throws IOException { /** * reads the contents of an InputStream into a ByteArrayOutputStream. */ - public static ByteArrayOutputStream read(InputStream is) throws IOException { + private static ByteArrayOutputStream read(InputStream is) throws IOException { ByteArrayOutputStream out = new ByteArrayOutputStream(); copy(is, out); return out; @@ -229,7 +232,7 @@ public static void deleteFilesInDirectory(File dir, boolean deleteDir) { /** * Compute MD5 checksum of the given path's file */ - public static byte[] getMD5Checksum(String path) { + private static byte[] getMD5Checksum(String path) { return getMD5Checksum(new File(path)); } @@ -251,9 +254,7 @@ public static byte[] getMD5Checksum(File file) { } return md.digest(); - } catch (NoSuchAlgorithmException e) { - Log.e(TAG, e.getMessage()); - } catch (IOException e) { + } catch (NoSuchAlgorithmException | IOException e) { Log.e(TAG, e.getMessage()); } finally { close(in); @@ -282,12 +283,12 @@ public static String hexMd5(File file) { * that the two of them are the same, the datetime contained in their exif * metadata will be compared. If the exif does not contain a datetime, the * MD5 checksum of the images will be compared. - * + * * @param image1 Absolute path to the first image * @param image2 Absolute path to the second image * @return true if their datetime is the same, false otherwise */ - public static boolean compareImages(String image1, String image2) { + private static boolean compareImages(String image1, String image2) { boolean equals = false; try { ExifInterface exif1 = new ExifInterface(image1); @@ -314,12 +315,12 @@ public static boolean compareImages(String image1, String image2) { * the two of them are the same, the MD5 checksum will be compared. Note * that if any of the files does not exist, or if its checksum cannot be * computed, false will be returned. - * + * * @param path1 Absolute path to the first file * @param path2 Absolute path to the second file * @return true if their MD5 checksum is the same, false otherwise. */ - public static boolean compareFilesChecksum(String path1, String path2) { + private static boolean compareFilesChecksum(String path1, String path2) { final byte[] checksum1 = getMD5Checksum(path1); final byte[] checksum2 = getMD5Checksum(path2); @@ -331,13 +332,13 @@ public static boolean compareFilesChecksum(String path1, String path2) { * folder. This method will try to spot those situations and remove the * duplicated image. * - * @param context Context + * @param context Context * @param filepath The absolute path to the original image */ public static void cleanDCIM(Context context, String filepath) { Cursor cursor = context.getContentResolver().query( MediaStore.Images.Media.EXTERNAL_CONTENT_URI, - new String[]{ + new String[] { MediaStore.Images.ImageColumns.DATA, MediaStore.Images.ImageColumns.DATE_TAKEN }, @@ -377,10 +378,9 @@ public static void cleanDCIM(Context context, String filepath) { * * @return the path and version of a newer APK, if found, null otherwise */ - public static String checkDownloadedVersions(Context context) { - final String installedVer = PlatformUtil.getVersionName(context); + public static String checkDownloadedVersions() { - String maxVersion = installedVer;// Keep track of newest version available + String maxVersion = BuildConfig.VERSION_NAME;// Keep track of newest version available String apkPath = null; File appsLocation = getFilesDir(FileType.APK); @@ -399,7 +399,7 @@ public static String checkDownloadedVersions(Context context) { apk.delete(); } version.delete(); - } else if (apks.length > 0){ + } else if (apks.length > 0) { maxVersion = versionName; apkPath = apks[0].getAbsolutePath();// There should only be 1 } diff --git a/app/src/main/java/org/akvo/flow/util/HttpUtil.java b/app/src/main/java/org/akvo/flow/util/HttpUtil.java index 2a80835ec..6e10d1217 100644 --- a/app/src/main/java/org/akvo/flow/util/HttpUtil.java +++ b/app/src/main/java/org/akvo/flow/util/HttpUtil.java @@ -1,23 +1,26 @@ /* - * Copyright (C) 2010-2012 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * - * This file is part of Akvo FLOW. + * This file is part of Akvo FLOW. * - * Akvo FLOW is free software: you can redistribute it and modify it under the terms of - * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, - * either version 3 of the License or any later version. + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. * - * Akvo FLOW 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 Affero General Public License included below for more details. + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . * - * The full license text can also be seen at . */ package org.akvo.flow.util; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.util.Log; +import org.akvo.flow.exception.HttpException; + import java.io.BufferedInputStream; import java.io.BufferedOutputStream; import java.io.BufferedReader; @@ -35,24 +38,24 @@ import java.util.Map; import java.util.Map.Entry; -import org.akvo.flow.exception.HttpException; -import org.apache.http.HttpStatus; - /** * Simple utility to make http calls and read the responses - * + * * @author Christopher Fagiani */ public class HttpUtil { + private static final String TAG = HttpUtil.class.getSimpleName(); private static final int BUFFER_SIZE = 8192; + @NonNull public static String httpGet(String url) throws IOException { HttpURLConnection conn = (HttpURLConnection) (new URL(url).openConnection()); final long t0 = System.currentTimeMillis(); + try { int status = getStatusCode(conn); - if (status != HttpStatus.SC_OK) { + if (status != HttpURLConnection.HTTP_OK) { throw new HttpException(conn.getResponseMessage(), status); } InputStream in = new BufferedInputStream(conn.getInputStream()); @@ -66,7 +69,7 @@ public static String httpGet(String url) throws IOException { } } - public static void httpGet(String url, File dst) throws IOException { + public static void httpGet(String url, @NonNull File dst) throws IOException { InputStream in = null; OutputStream out = null; HttpURLConnection conn = null; @@ -79,7 +82,7 @@ public static void httpGet(String url, File dst) throws IOException { copyStream(in, out); int status = conn.getResponseCode(); - if (status != HttpStatus.SC_OK) { + if (status != HttpURLConnection.HTTP_OK) { // TODO: Use custom exception? throw new IOException("Status Code: " + status + ". Expected: 200 - OK"); } @@ -98,7 +101,7 @@ public static void httpGet(String url, File dst) throws IOException { public static String httpPost(String url, Map params) throws IOException { OutputStream out = null; InputStream in = null; - Writer writer = null; + Writer writer; HttpURLConnection conn = null; try { conn = (HttpURLConnection) new URL(url).openConnection(); @@ -115,7 +118,7 @@ public static String httpPost(String url, Map params) throws IOE in = new BufferedInputStream(conn.getInputStream()); int status = getStatusCode(conn); - if (status != HttpStatus.SC_OK) { + if (status != HttpURLConnection.HTTP_OK) { throw new HttpException(conn.getResponseMessage(), status); } return readStream(in); @@ -128,7 +131,7 @@ public static String httpPost(String url, Map params) throws IOE } } - public static int getStatusCode(HttpURLConnection conn) throws IOException { + private static int getStatusCode(@NonNull HttpURLConnection conn) throws IOException { try { return conn.getResponseCode(); } catch (IOException e) { @@ -140,7 +143,8 @@ public static int getStatusCode(HttpURLConnection conn) throws IOException { } } - public static String getQuery(Map params) { + @NonNull + private static String getQuery(@Nullable Map params) { if (params == null) { return ""; } @@ -153,7 +157,7 @@ public static String getQuery(Map params) { return builder.length() > 0 ? builder.substring(1) : builder.toString(); } - public static String readStream(InputStream in) throws IOException { + private static String readStream(@NonNull InputStream in) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader(in)); StringBuilder builder = new StringBuilder(); @@ -169,7 +173,8 @@ public static String readStream(InputStream in) throws IOException { return builder.toString(); } - public static void copyStream(InputStream in, OutputStream out) throws IOException { + public static void copyStream(@NonNull InputStream in, @NonNull OutputStream out) + throws IOException { byte[] b = new byte[BUFFER_SIZE]; int read; while ((read = in.read(b)) != -1) { diff --git a/app/src/main/java/org/akvo/flow/util/NotificationHelper.java b/app/src/main/java/org/akvo/flow/util/NotificationHelper.java new file mode 100644 index 000000000..de906cb11 --- /dev/null +++ b/app/src/main/java/org/akvo/flow/util/NotificationHelper.java @@ -0,0 +1,151 @@ +/* + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + * + */ + +package org.akvo.flow.util; + +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.content.Context; +import android.content.Intent; +import android.support.v4.app.NotificationCompat; +import android.support.v4.content.ContextCompat; + +import org.akvo.flow.R; + +public class NotificationHelper { + + private NotificationHelper() { + } + + /** + * Displays a notification in the system status bar + * + * @param title - headline to display in notification bar + * @param text - body of notification (when user expands bar) + * @param notificationId - unique (within app) ID of notification + */ + public static void displayNotification(String title, String text, Context context, int notificationId) { + NotificationCompat.Builder builder = + createNotificationBuilder(title, text, context); + notifyWithDummyIntent(context, notificationId, builder); + } + + /** + * Displays a notification in the system status bar + * + * @param title - headline to display in notification bar + * @param text - body of notification (when user expands bar) + * @param notificationId - unique (within app) ID of notification + */ + public static void displayErrorNotification(String title, String text, Context context, int notificationId) { + NotificationCompat.Builder builder = + createErrorNotificationBuilder(title, text, context); + notifyWithDummyIntent(context, notificationId, builder); + } + + + public static void displayNonOnGoingErrorNotification(Context context, int notificationId, String text, String title) { + NotificationCompat.Builder builder = createErrorNotificationBuilder(title, text, context); + builder.setOngoing(false); + + notifyWithDummyIntent(context, notificationId, builder); + } + + public static void displayProgressNotification(Context context, int synced, int total, String title, String text, + int notificationId) { + NotificationCompat.Builder builder = createNotificationBuilder(title, text, context); + builder.setOngoing(true); + + // Progress will only be displayed in Android versions > 4.0 + builder.setProgress(total, synced, false); + + notifyWithDummyIntent(context, notificationId, builder); + } + + public static void displayNonOngoingNotificationWithProgress(Context context, String text, String title, + int notificationId) { + NotificationCompat.Builder builder = createNotificationBuilder(title, text, context); + builder.setOngoing(false); + + // Progress will only be displayed in Android versions > 4.0 + builder.setProgress(1, 1, false); + + notifyWithDummyIntent(context, notificationId, builder); + } + + public static void displayNotification(Context context, int total, String title, String text, int notificationId, + boolean ongoing, int progress) { + NotificationCompat.Builder builder = createNotificationBuilder(title, text, context); + + builder.setOngoing(ongoing);// Ongoing if still syncing the records + + // Progress will only be displayed in Android versions > 4.0 + builder.setProgress(total, progress, false); + + notifyWithDummyIntent(context, notificationId, builder); + } + + public static void displayNotificationWithProgress(Context context, String title, String text, boolean ongoing, + boolean indeterminate, int notificationId) { + NotificationCompat.Builder builder = createNotificationBuilder(title, text, context); + + builder.setOngoing(ongoing); // Ongoing if still syncing the records + + // Progress will only be displayed in Android versions > 4.0 + builder.setProgress(1, 1, indeterminate); + + notifyWithDummyIntent(context, notificationId, builder); + } + + public static void displayErrorNotificationWithProgress(Context context, String title, String text, boolean ongoing, + boolean indeterminate, int notificationId) { + NotificationCompat.Builder builder = createErrorNotificationBuilder(title, text, context); + + builder.setOngoing(ongoing); // Ongoing if still syncing the records + + // Progress will only be displayed in Android versions > 4.0 + builder.setProgress(1, 1, indeterminate); + + notifyWithDummyIntent(context, notificationId, builder); + } + + private static void notifyWithDummyIntent(Context context, int notificationId, NotificationCompat.Builder builder) { + // Dummy intent. Do nothing when clicked + PendingIntent dummyIntent = PendingIntent.getActivity(context, 0, new Intent(), 0); + builder.setContentIntent(dummyIntent); + + NotificationManager notificationManager = + (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); + notificationManager.notify(notificationId, builder.build()); + } + + private static NotificationCompat.Builder createNotificationBuilder(String title, String text, Context context) { + return createDefaultNotification(title, text, context) + .setColor(ContextCompat.getColor(context, R.color.orange_main)); + } + + private static NotificationCompat.Builder createDefaultNotification(String title, String text, Context context) { + return new NotificationCompat.Builder(context).setSmallIcon(R.drawable.notification_icon) + .setStyle(new NotificationCompat.BigTextStyle().bigText(text)) + .setContentTitle(title) + .setContentText(text) + .setTicker(title); + } + + private static NotificationCompat.Builder createErrorNotificationBuilder(String title, String text, Context context) { + return createDefaultNotification(title, text, context) + .setColor(ContextCompat.getColor(context, R.color.red)); + } +} diff --git a/app/src/main/java/org/akvo/flow/util/PlatformUtil.java b/app/src/main/java/org/akvo/flow/util/PlatformUtil.java index b023502ce..a9744fbf6 100644 --- a/app/src/main/java/org/akvo/flow/util/PlatformUtil.java +++ b/app/src/main/java/org/akvo/flow/util/PlatformUtil.java @@ -18,12 +18,11 @@ import android.content.Context; import android.content.Intent; -import android.content.pm.PackageManager.NameNotFoundException; import android.content.res.Resources; import android.content.res.TypedArray; import android.net.Uri; import android.provider.Settings.Secure; -import android.util.Log; +import android.support.annotation.Nullable; import android.util.TypedValue; import java.io.File; @@ -33,25 +32,11 @@ * Utilities class to provide Android related functionalities */ public class PlatformUtil { - private static final String TAG = PlatformUtil.class.getSimpleName(); - - /** - * Get the version name assigned in AndroidManifest.xml - * - * @param context - * @return versionName - */ - public static String getVersionName(Context context) { - try { - return context.getPackageManager().getPackageInfo( - context.getPackageName(), 0).versionName; - } catch (NameNotFoundException e) { - Log.e(TAG, e.getMessage()); - return ""; - } - } /** + * TODO: use versionCode to compare versions as versionName field does not have to be X.Y.Z + * format + * * Check if a given version is newer than the current one. * Versions are expected to be formatted in a dot-decimal notation: X.Y.Z, * being X, Y, and Z integers, and each number separated by a full stop (dot). @@ -60,7 +45,11 @@ public static String getVersionName(Context context) { * @param newVersion * @return true if the second version is newer than the first one, false otherwise */ - public static boolean isNewerVersion(String installedVersion, String newVersion) { + public static boolean isNewerVersion(@Nullable String installedVersion, + @Nullable String newVersion) { + if (installedVersion == null || newVersion == null) { + return false; + } // Ensure the Strings are properly formatted final String regex = "^\\d+(\\.\\d+)*$";// Check dot-decimal notation if (!installedVersion.matches(regex) || !newVersion.matches(regex)) { @@ -90,7 +79,7 @@ public static float dp2Pixel(Context context, int dp) { } public static int getResource(Context context, int attr) { - TypedArray a = context.getTheme().obtainStyledAttributes(new int[]{attr}); + TypedArray a = context.getTheme().obtainStyledAttributes(new int[] { attr }); return a.getResourceId(0, 0); } @@ -98,7 +87,8 @@ public static int getResource(Context context, int attr) { * Install the newest version of the app. This method will be called * either after the file download is completed, or upon the app being started, * if the newest version is found in the filesystem. - * @param context Context + * + * @param context Context * @param filename Absolute path to the newer APK */ public static void installAppUpdate(Context context, String filename) { @@ -109,14 +99,15 @@ public static void installAppUpdate(Context context, String filename) { context.startActivity(intent); } - public static String uuid(){ + public static String uuid() { return UUID.randomUUID().toString(); } - public static String recordUuid(){ + public static String recordUuid() { String base32Id = Base32.base32Uuid(); // Put dashes between the 4-5 and 8-9 positions to increase readability - return base32Id.substring(0, 4) + "-" + base32Id.substring(4, 8) + "-" + base32Id.substring(8); + return base32Id.substring(0, 4) + "-" + base32Id.substring(4, 8) + "-" + base32Id + .substring(8); } public static String getAndroidID(Context context) { diff --git a/app/src/main/java/org/akvo/flow/util/Prefs.java b/app/src/main/java/org/akvo/flow/util/Prefs.java index 862984282..dd89d49cc 100644 --- a/app/src/main/java/org/akvo/flow/util/Prefs.java +++ b/app/src/main/java/org/akvo/flow/util/Prefs.java @@ -1,3 +1,20 @@ +/* + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) + * + * This file is part of Akvo FLOW. + * + * Akvo FLOW is free software: you can redistribute it and modify it under the terms of + * the GNU Affero General Public License (AGPL) as published by the Free Software Foundation, + * either version 3 of the License or any later version. + * + * Akvo FLOW 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 Affero General Public License included below for more details. + * + * The full license text can also be seen at . + * + */ + package org.akvo.flow.util; import android.content.Context; @@ -8,6 +25,7 @@ * access and edit key/value pairs. */ public class Prefs { + public static final String KEY_SURVEY_GROUP_ID = "surveyGroupId"; public static final String KEY_USER_ID = "userId"; public static final String KEY_SETUP = "setup"; @@ -15,41 +33,50 @@ public class Prefs { private static final String PREFS_NAME = "flow_prefs"; private static final int PREFS_MODE = Context.MODE_PRIVATE; - private static SharedPreferences getPrefs(Context context) { + private final Context context; + + public Prefs(Context context) { + this.context = context; + } + + private SharedPreferences getPrefs() { return context.getSharedPreferences(PREFS_NAME, PREFS_MODE); } - public static String getString(Context context, String key, String defValue) { - return getPrefs(context).getString(key, defValue); + public String getString(String key, String defValue) { + return getPrefs().getString(key, defValue); } - public static void setString(Context context, String key, String value) { - getPrefs(context).edit().putString(key, value).commit(); + public void setString(String key, String value) { + getPrefs().edit().putString(key, value).apply(); } - public static boolean getBoolean(Context context, String key, boolean defValue) { - return getPrefs(context).getBoolean(key, defValue); + public boolean getBoolean(String key, boolean defValue) { + return getPrefs().getBoolean(key, defValue); } - public static void setBoolean(Context context, String key, boolean value) { - getPrefs(context).edit().putBoolean(key, value).commit(); + public void setBoolean(String key, boolean value) { + getPrefs().edit().putBoolean(key, value).apply(); } - public static long getLong(Context context, String key, long defValue) { - return getPrefs(context).getLong(key, defValue); + public long getLong(String key, long defValue) { + return getPrefs().getLong(key, defValue); } - public static void setLong(Context context, String key, long value) { - getPrefs(context).edit().putLong(key, value).commit(); + public void setLong(String key, long value) { + getPrefs().edit().putLong(key, value).apply(); } - public static int getInt(Context context, String key, int defValue) { - return getPrefs(context).getInt(key, defValue); + public int getInt(String key, int defValue) { + return getPrefs().getInt(key, defValue); } - public static void setInt(Context context, String key, int value) { - getPrefs(context).edit().putInt(key, value).commit(); + public void setInt(String key, int value) { + getPrefs().edit().putInt(key, value).apply(); } + public void removePreference(String key) { + getPrefs().edit().remove(key).apply(); + } } diff --git a/app/src/main/java/org/akvo/flow/util/StatusUtil.java b/app/src/main/java/org/akvo/flow/util/StatusUtil.java index 195b576bb..5f1305145 100644 --- a/app/src/main/java/org/akvo/flow/util/StatusUtil.java +++ b/app/src/main/java/org/akvo/flow/util/StatusUtil.java @@ -70,6 +70,28 @@ public static boolean hasDataConnection(Context context) { return false; } + /** + * Checks whether or not we are allowed to connect + * + * @param context + * @return + */ + public static boolean isConnectionAllowed(Context context) { + //user allowed 3g usage + if (StatusUtil.syncOver3G(context)) { + return true; + } + //only if wifi is connected can we attempt a connection + return isWifiConnected(context); + } + + private static boolean isWifiConnected(Context context) { + ConnectivityManager connectionManager = (ConnectivityManager) context + .getSystemService(Context.CONNECTIVITY_SERVICE); + NetworkInfo wifiCheck = connectionManager.getNetworkInfo(ConnectivityManager.TYPE_WIFI); + return wifiCheck.isConnected(); + } + /** * gets the device's primary phone number * diff --git a/app/src/main/java/org/akvo/flow/util/StringUtil.java b/app/src/main/java/org/akvo/flow/util/StringUtil.java index 832428ea0..a6bbe07ff 100644 --- a/app/src/main/java/org/akvo/flow/util/StringUtil.java +++ b/app/src/main/java/org/akvo/flow/util/StringUtil.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2010-2012 Stichting Akvo (Akvo Foundation) + * Copyright (C) 2010-2016 Stichting Akvo (Akvo Foundation) * * This file is part of Akvo FLOW. * @@ -16,6 +16,7 @@ package org.akvo.flow.util; +import android.support.annotation.Nullable; import android.text.TextUtils; /** @@ -25,37 +26,28 @@ */ public class StringUtil { + public static final char SPACE_CHAR = '\u0020'; + /** * checks a string to see if it's null or has no non-whitespace characters * * @param s * @return */ - public static boolean isNullOrEmpty(String s) { + public static boolean isNullOrEmpty(@Nullable String s) { return s == null || s.trim().length() == 0; } // copy a string transforming all control chars // (like newline and tab) into spaces - public static String ControlToSPace(String val) { + public static String controlToSpace(@Nullable String val) { String result = ""; - for (int i = 0; i < val.length(); i++) { - if (val.charAt(i) < '\u0020') - result = result + '\u0020'; - else - result = result + val.charAt(i); + if (val == null) { + return result; } - - return result; - } - - // copy a string transforming all control chars - // (like newline and tab) and comma into spaces - public static String ControlCommaToSPace(String val) { - String result = ""; for (int i = 0; i < val.length(); i++) { - if (val.charAt(i) < '\u0020' || val.charAt(i) == ',') - result = result + '\u0020'; + if (val.charAt(i) < SPACE_CHAR) + result = result + SPACE_CHAR; else result = result + val.charAt(i); } @@ -63,7 +55,7 @@ public static String ControlCommaToSPace(String val) { return result; } - public static boolean isValid(String value) { + public static boolean isValid(@Nullable String value) { return !TextUtils.isEmpty(value) && !value.equalsIgnoreCase("null"); } } diff --git a/app/src/main/java/org/akvo/flow/util/ViewUtil.java b/app/src/main/java/org/akvo/flow/util/ViewUtil.java index f2a8b651b..0b3ff202b 100644 --- a/app/src/main/java/org/akvo/flow/util/ViewUtil.java +++ b/app/src/main/java/org/akvo/flow/util/ViewUtil.java @@ -17,14 +17,11 @@ package org.akvo.flow.util; import android.app.AlertDialog; -import android.app.NotificationManager; -import android.app.PendingIntent; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Handler; import android.support.annotation.NonNull; -import android.support.v4.app.NotificationCompat; import android.view.ViewGroup.LayoutParams; import android.widget.EditText; import android.widget.LinearLayout; @@ -34,7 +31,7 @@ /** * Utility class to handle common features for the View tier - * + * * @author Christopher Fagiani */ public class ViewUtil { @@ -44,7 +41,7 @@ public class ViewUtil { * the affirmative button is clicked, the Location Settings panel is * launched. If the negative button is clicked, it will just close the * dialog - * + * * @param parentContext */ public static void showGPSDialog(final Context parentContext) { @@ -71,12 +68,12 @@ public void onClick(DialogInterface dialog, int id) { /** * displays a simple dialog box with only a single, positive button using * the resource ids of the strings passed in for the title and text. - * + * * @param titleId * @param textId * @param parentContext */ - public static void showConfirmDialog(int titleId, int textId, + private static void showConfirmDialog(int titleId, int textId, Context parentContext) { showConfirmDialog(titleId, textId, parentContext, false, new DialogInterface.OnClickListener() { @@ -92,7 +89,7 @@ public void onClick(DialogInterface dialog, int id) { * displays a simple dialog box with a single positive button and an * optional (based on a flag) cancel button using the resource ids of the * strings passed in for the title and text. - * + * * @param titleId * @param textId * @param parentContext @@ -116,17 +113,17 @@ public void onClick(DialogInterface dialog, int which) { * optional (based on a flag) cancel button using the resource ids of the * strings passed in for the title and text. users can install listeners for * both the positive and negative buttons - * + * * @param titleId * @param textId * @param parentContext * @param includeNegative * @param positiveListener - if includeNegative is false, this will also be - * bound to the cancel handler + * bound to the cancel handler * @param negativeListener - only used if includeNegative is true - if the - * negative listener is non-null, it will also be bound to the - * cancel listener so pressing back to dismiss the dialog will - * have the same effect as clicking the negative button. + * negative listener is non-null, it will also be bound to the + * cancel listener so pressing back to dismiss the dialog will + * have the same effect as clicking the negative button. */ public static void showConfirmDialog(int titleId, int textId, Context parentContext, boolean includeNegative, @@ -163,17 +160,17 @@ public void onCancel(DialogInterface dialog) { * optional (based on a flag) cancel button using the resource id of the * string passed in for the title, and a String parameter for the text. * users can install listeners for both the positive and negative buttons - * + * * @param titleId * @param text * @param parentContext * @param includeNegative * @param positiveListener - if includeNegative is false, this will also be - * bound to the cancel handler + * bound to the cancel handler * @param negativeListener - only used if includeNegative is true - if the - * negative listener is non-null, it will also be bound to the - * cancel listener so pressing back to dismiss the dialog will - * have the same effect as clicking the negative button. + * negative listener is non-null, it will also be bound to the + * cancel listener so pressing back to dismiss the dialog will + * have the same effect as clicking the negative button. */ public static void showConfirmDialog(int titleId, String text, Context parentContext, boolean includeNegative, @@ -205,32 +202,6 @@ public void onCancel(DialogInterface dialog) { builder.show(); } - /** - * Displays a notification in the system status bar - * - * @param title - headline to display in notification bar - * @param text - body of notification (when user expands bar) - * @param context - * @param id - unique (within app) ID of notification - */ - public static void displayNotification(String title, String text, - Context context, int id, Integer iconId) { - NotificationCompat.Builder builder = new NotificationCompat.Builder(context) - .setContentTitle(title) - .setTicker(title) - .setContentText(text) - .setSmallIcon(iconId != null ? iconId : R.drawable.info) - .setStyle(new NotificationCompat.BigTextStyle().bigText(text)); - - // Dummy intent. Do nothing when clicked - PendingIntent dummyIntent = PendingIntent.getActivity(context, 0, new Intent(), 0); - builder.setContentIntent(dummyIntent); - - NotificationManager notificationManager = - (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); - notificationManager.notify(id, builder.build()); - } - /** * displays a dialog box for selection of one or more survey languages */ @@ -348,7 +319,7 @@ public static void ShowTextInputDialog(final Context parentContext, DialogInterface.OnClickListener clickListener) { AlertDialog.Builder builder = new AlertDialog.Builder(parentContext); LinearLayout main = new LinearLayout(parentContext); - main.setLayoutParams(new LayoutParams(LayoutParams.FILL_PARENT, + main.setLayoutParams(new LayoutParams(LayoutParams.MATCH_PARENT, LayoutParams.WRAP_CONTENT)); main.setOrientation(LinearLayout.VERTICAL); builder.setTitle(title); @@ -372,12 +343,14 @@ public void onClick(DialogInterface dialog, int which) { /** * Display a UI Toast using the Handler's thread (main thread) - * @param msg message to display - * @param uiThreadHandler the handler to use + * + * @param msg message to display + * @param uiThreadHandler the handler to use * @param applicationContext the Context to use for the toast */ - public static void displayToastFromService(@NonNull final String msg, @NonNull Handler uiThreadHandler, - @NonNull final Context applicationContext) { + public static void displayToastFromService(@NonNull final String msg, + @NonNull Handler uiThreadHandler, + @NonNull final Context applicationContext) { uiThreadHandler.post(new ServiceToastRunnable(applicationContext, msg)); } @@ -388,5 +361,5 @@ public static void displayToastFromService(@NonNull final String msg, @NonNull H public interface AdminAuthDialogListener { void onAuthenticated(); } - + } diff --git a/app/src/main/res/color/repetitions_text_color.xml b/app/src/main/res/color/repetitions_text_color.xml new file mode 100644 index 000000000..5841c264e --- /dev/null +++ b/app/src/main/res/color/repetitions_text_color.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/text_color_secondary.xml b/app/src/main/res/color/text_color_secondary.xml deleted file mode 100644 index 81e2ad2d5..000000000 --- a/app/src/main/res/color/text_color_secondary.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/color/text_color_tertiary.xml b/app/src/main/res/color/text_color_tertiary.xml deleted file mode 100644 index 09ed68776..000000000 --- a/app/src/main/res/color/text_color_tertiary.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/res/drawable-hdpi-v11/notification_icon.png b/app/src/main/res/drawable-hdpi-v11/notification_icon.png new file mode 100644 index 000000000..8cdb19fb2 Binary files /dev/null and b/app/src/main/res/drawable-hdpi-v11/notification_icon.png differ diff --git a/app/src/main/res/drawable-hdpi/ic_arrow_back_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_arrow_back_white_48dp.png deleted file mode 100644 index 746d77579..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_arrow_back_white_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/ic_menu_white_48dp.png b/app/src/main/res/drawable-hdpi/ic_menu_white_48dp.png deleted file mode 100644 index 9cb034823..000000000 Binary files a/app/src/main/res/drawable-hdpi/ic_menu_white_48dp.png and /dev/null differ diff --git a/app/src/main/res/drawable-hdpi/notification_icon.png b/app/src/main/res/drawable-hdpi/notification_icon.png new file mode 100644 index 000000000..e25743341 Binary files /dev/null and b/app/src/main/res/drawable-hdpi/notification_icon.png differ diff --git a/app/src/main/res/drawable-mdpi-v11/notification_icon.png b/app/src/main/res/drawable-mdpi-v11/notification_icon.png new file mode 100644 index 000000000..df807cc23 Binary files /dev/null and b/app/src/main/res/drawable-mdpi-v11/notification_icon.png differ diff --git a/app/src/main/res/drawable-mdpi/notification_icon.png b/app/src/main/res/drawable-mdpi/notification_icon.png new file mode 100644 index 000000000..bdd64c8af Binary files /dev/null and b/app/src/main/res/drawable-mdpi/notification_icon.png differ diff --git a/app/src/main/res/drawable-xhdpi-v11/notification_icon.png b/app/src/main/res/drawable-xhdpi-v11/notification_icon.png new file mode 100644 index 000000000..62cdee531 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi-v11/notification_icon.png differ diff --git a/app/src/main/res/drawable-xhdpi/notification_icon.png b/app/src/main/res/drawable-xhdpi/notification_icon.png new file mode 100644 index 000000000..81bf50f6e Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/notification_icon.png differ diff --git a/app/src/main/res/drawable/image_button_background.xml b/app/src/main/res/drawable/image_button_background.xml new file mode 100644 index 000000000..7f986e409 --- /dev/null +++ b/app/src/main/res/drawable/image_button_background.xml @@ -0,0 +1,7 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image_button_disabled.xml b/app/src/main/res/drawable/image_button_disabled.xml new file mode 100644 index 000000000..9e0553c52 --- /dev/null +++ b/app/src/main/res/drawable/image_button_disabled.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image_button_normal.xml b/app/src/main/res/drawable/image_button_normal.xml new file mode 100644 index 000000000..beb52085c --- /dev/null +++ b/app/src/main/res/drawable/image_button_normal.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/image_button_selected.xml b/app/src/main/res/drawable/image_button_selected.xml new file mode 100644 index 000000000..5224529f5 --- /dev/null +++ b/app/src/main/res/drawable/image_button_selected.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cascade_spinner_item.xml b/app/src/main/res/layout/cascade_spinner_item.xml new file mode 100644 index 000000000..4cf0abdad --- /dev/null +++ b/app/src/main/res/layout/cascade_spinner_item.xml @@ -0,0 +1,14 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/cascading_level_item.xml b/app/src/main/res/layout/cascading_level_item.xml index 2e3cc7ec9..8e9255e2a 100644 --- a/app/src/main/res/layout/cascading_level_item.xml +++ b/app/src/main/res/layout/cascading_level_item.xml @@ -1,18 +1,21 @@ + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:orientation="vertical"> + android:id="@+id/text" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:gravity="center_vertical" + android:textStyle="bold" + android:textSize="18sp"/> diff --git a/app/src/main/res/layout/datapoints_fragment.xml b/app/src/main/res/layout/datapoints_fragment.xml index 1f5b97a17..164d63351 100644 --- a/app/src/main/res/layout/datapoints_fragment.xml +++ b/app/src/main/res/layout/datapoints_fragment.xml @@ -7,7 +7,7 @@ + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent"> + android:id="@+id/map" + android:name="com.google.android.gms.maps.SupportMapFragment" + android:layout_width="match_parent" + android:layout_height="match_parent"/> + android:id="@+id/accuracy" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:padding="8dp" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_alignParentTop="true" + android:textSize="16sp"/> + android:id="@+id/feature_menu" + android:layout_width="match_parent" + android:layout_height="48dp" + android:layout_alignParentBottom="true" + android:background="@color/feature_menu_background" + android:visibility="gone" + tools:visibility="visible"> + android:id="@+id/feature_name" + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentLeft="true" + android:layout_alignParentStart="true" + android:layout_marginLeft="10dp" + android:layout_marginStart="10dp" + android:gravity="center" + android:textSize="18sp" + android:textColor="@color/white"/> + android:layout_width="wrap_content" + android:layout_height="match_parent" + android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" + android:gravity="center"> + android:id="@+id/properties" + android:src="@drawable/ic_info_path" + style="@style/ImageButtonStyle"/> + android:id="@+id/add_point_btn" + android:src="@drawable/ic_add_point" + style="@style/ImageButtonStyle"/> + android:id="@+id/clear_point_btn" + android:src="@drawable/ic_delete_point" + tools:enabled="false" + style="@style/ImageButtonStyle"/> + android:id="@+id/clear_feature_btn" + android:src="@drawable/ic_delete_feature" + style="@style/ImageButtonStyle"/> diff --git a/app/src/main/res/layout/preferences.xml b/app/src/main/res/layout/preferences.xml index 608fdb3c8..b36d357ba 100644 --- a/app/src/main/res/layout/preferences.xml +++ b/app/src/main/res/layout/preferences.xml @@ -22,6 +22,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:saveEnabled="false"/> @@ -37,6 +39,7 @@ android:layout_height="wrap_content" android:layout_below="@id/screenopttxt" android:layout_toLeftOf="@id/screenoptcheckbox" + android:layout_toStartOf="@id/screenoptcheckbox" android:text="@string/screenoptdesc" android:textSize="14sp" /> @@ -54,6 +57,7 @@ android:layout_height="wrap_content" android:layout_centerVertical="true" android:layout_alignParentRight="true" + android:layout_alignParentEnd="true" android:saveEnabled="false"/> @@ -62,6 +66,7 @@ android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_toLeftOf="@id/uploadoptioncheckbox" + android:layout_toStartOf="@id/uploadoptioncheckbox" android:layout_centerVertical="true" android:text="@string/uploadoptionlabel" android:textSize="20sp" /> @@ -117,31 +122,6 @@ android:textSize="14sp" /> - - - - - - - + android:layout_width="match_parent" + android:layout_height="wrap_content" + xmlns:tools="http://schemas.android.com/tools" + android:minHeight="?android:attr/listPreferredItemHeight" + android:gravity="center_vertical" + android:padding="8dp"> - - + android:textColor="@color/text_color_primary" + tools:text="Farmer Rice Diary: Pest - Disease" + tools:textSize="22sp" /> - - Akvo FLOW + Prec Buscando... Listo @@ -31,11 +30,8 @@ Evitar que la pantalla se apague durante la introducción de datos. Asegúrese de apagar la pantalla (o cerrar la aplicación) cuando no esté en uso, para reducir el consumo de batería. Acerca de Muestra información sobre la versión actual - Acerca de - Akvo FLOW app - Akvo FLOW app -Desarrollada por: Akvo Foundation -Copyright © 2010-2014 Stichting Akvo -Version + Acerca de - Akvo Flow app + Akvo Flow app\nDesarrollada por: Akvo Foundation\nCopyright © 2010-%1$s Stichting Akvo\nVersion %2$s Estado del GPS Abrir la aplicación GPS Status si la tiene instalada en este dispositivo. GPS Status no está instalada en este dispositivo. Por favor, instale la aplicación a través de Google Play Store. @@ -49,7 +45,6 @@ Version Mantener entre sesiones el último usuario activo Enviar datos mediante red móvil Idiomas de la encuesta - Enviar localización Escanear código de barras Barcode Scanner no está instalado. Por favor, instale Barcode Scanner a través de Play Store. El valor es demasiado pequeño. Valor mínimo: @@ -89,7 +84,7 @@ Version Cargando datos de la encuesta desde la tarjeta SD Datos cargados desde la tarjeta SD Ha ocurrido un error procesando el archivo zip bootstrap - Error: El formulario no pertenece a esta instancia FLOW + Error: El formulario no pertenece a esta instancia Flow Historial de transmisión Inicio: Fin: @@ -117,21 +112,24 @@ Version Los Puntos de Datos han sido sincronizados %1$d Puntos de Datos sincronizados Sincronización de Puntos de Datos fallida + Vuelva a sincronizar. Error de conexión Cargando... Pre-llenar respuestas Pre-llenar respuestas con previos valores de las mismas preguntas - Ha de crear una asignación en el Panel de Control FLOW + Ha de crear una asignación en el Panel de Control Flow Sincronización de datos Sincronizando datos. Por favor, espere... Formularios sincronizados: %1$d - Fallidos: %2$d Formularios sincronizados: %1$d + Formulario borrado + El formulario %1$s ha sido borrado Idioma de la aplicación Por favor, reinicie la aplicación - No tiene encuestas asignadas. Las encuestas han de ser asignadas en el Panel de Control FLOW + No tiene encuestas asignadas. Las encuestas han de ser asignadas en el Panel de Control Flow Aún no dispone de puntos de datos. Clique \'+\' en el menú de arriba para añadir un nuevo punto de datos No ha seleccionado ninguna Encuesta. Abra el menú lateral para seleccionar una. @@ -205,12 +203,12 @@ Version Finalizado Exportado - Actualización Akvo FLOW - Hay una nueva versión de la aplicación FLOW disponible. Pulse \'Actualizar\' para descargar e instalar la nueva versión. + Actualización Akvo Flow + Hay una nueva versión de la aplicación Flow disponible. Pulse \'Actualizar\' para descargar e instalar la nueva versión. Actualizar Ahora no - La nueva versión de FLOW está descargada. ¿Quiere instalarla ahora? - Error descargando la actualización de FLOW + La nueva versión de Flow está descargada. ¿Quiere instalarla ahora? + Error descargando la actualización de Flow Fecha y hora ¡La hora de su dispositivo no es correcta! Ajuste la hora y vuelta a intentar. La hora local de su dispositivo es: @@ -218,7 +216,7 @@ Version Descargando formularios Error sincronizando formularios - Las asignaciones de encuesta no se han podido recibir desde el panel de control FLOW + Las asignaciones de encuesta no se han podido recibir desde el panel de control Flow El formulario %1$s no se ha podido recibir desde el panel de control. Asegúrese que el formulario está publicado, y que la hora/fecha del dispositivo es correcta. Este formulario ha sido borrado. @@ -265,4 +263,5 @@ Version Ya que tiene la última versión de la aplicación. Se ha producido un error al intentar actualizar la aplicación, por favor intente-lo de nuevo. Se ha producido un error al intentar actualizar la aplicación, usted no está conectado a Internet. + Localización desconocida diff --git a/app/src/main/res/values-fr/strings.xml b/app/src/main/res/values-fr/strings.xml index 8fd43811a..6cab8ec9d 100644 --- a/app/src/main/res/values-fr/strings.xml +++ b/app/src/main/res/values-fr/strings.xml @@ -1,6 +1,5 @@ - - Akvo FLOW + Préc Recherche... Prêt @@ -16,7 +15,7 @@ Lon Altitude Envoyer - Votre GPS doit être activer. Cliquez OK pour accéder aux préférences. Sélectionnez Utilisez le GPS et pressez le bouton de retour pour revenir a cet écran. Vous pouvez maintenant obtenir la géolocalisation. + Votre GPS doit être activé. Cliquez OK pour accéder aux préférences. Sélectionnez Utilisez le GPS et pressez le bouton de retour pour revenir a cet écran. Vous pouvez maintenant obtenir la géolocalisation. Réduire les images trop lourdes Paramètres Il n\'y a pas d\'utilisateurs! @@ -31,8 +30,8 @@ Empêcher l\'écran de s\'éteindre pendant que vous remplissez les données. S\'assurer d\'éteindre l\'écran manuellement (ou quitter l\'application) quand ceci n\'est pas utilisé afin d\'économiser la batterie. A propos Information sur les versions. - A propos - Application Akvo FLOW - Akvo FLOW app\nDeveloped By: Akvo Foundation\nCopyright © 2010-2014 Stichting Akvo\nVersion + A propos - Application Akvo Flow + Application Akvo Flow\nDéveloppée par: Akvo Foundation\nTous Droits réservés © 2010-%1$s Stichting Akvo\nVersion %2$s Etat du GPS Démarre l\'application de GPS Status si elle est installée sur cet appareil. GPS Status n\'est pas installé sur cet appareil. Veuillez l\'installer en utilisant Google Play Store. @@ -46,7 +45,6 @@ Garde le dernier utilisateur connecté entre les sessions Envoyer les données avec le réseau mobile Langues de l\'enquête - Envoyer la localisation Scanner le code barre Le scanner n\'est pas installé. Veuillez installer l\'application de code barre sur Android Market. La valeur est trop petite. Valeur minimum: @@ -114,21 +112,24 @@ La synchronisation des enregistrements est terminée %1$d des enregistrements synchronisés La synchronisation des enregistrements à échouer + Veuillez synchroniser de nouveau. Erreur de réseau Chargement... Pré-remplir les réponses Pré-remplir les réponses avec les valeurs précédentes de la même question - Affectation requise sur le tableau de bord de Akvo FLOW + Affectation requise sur le tableau de bord de Akvo Flow Synchronisation des données Transfère en cours. Veuillez patienter Formulaires synchronisés: %1$d - Echoués: %2$d Formulaires synchronisés: %1$d + Ce formulaire a été supprimé. + Le formulaire %1$s a été supprimé Langue de l\'application Veuillez redémarrer l\'application - Vous n\'avez pas d\'enquête affectée à l\'appareil. Les enquêtes sont affectées depuis le tableau de bord FLOW + Vous n\'avez pas d\'enquête affectée à l\'appareil. Les enquêtes sont affectées depuis le tableau de bord Flow Vous n\'avez pas encore de données. Cliquez \"+\" en haut de l\'écran pour en ajouter. Vous n\'avez pas de questionnaire sélectionné. @@ -202,12 +203,12 @@ Envoyé Exporté - Mise à jour de Akvo FLOW - Une mise à jour de FLOW est disponible. Cliquez \"mettre à jour\" pour télécharger et installer la nouvelle version. + Mise à jour de Akvo Flow + Une mise à jour de Flow est disponible. Cliquez \"mettre à jour\" pour télécharger et installer la nouvelle version. Mise à jour Pas maintenant La mise à jour est téléchargée. Voulez vous l\'installer maintenant? - Erreur lors du téléchargement de la mise à jour de FLOW. + Erreur lors du téléchargement de la mise à jour de Flow. Date et heure Votre date de téléphone est inexacte! Réglez votre horloge et essayer à nouveau. Votre date et l\'heure téléphone est le: @@ -215,8 +216,8 @@ Téléchargement des formulaires Error en synchronisant les formulaires - Les affectations des formulaires n\'ont pas pas pu être lu du tableau de bord FLOW - Le formulaire %1$s n\'a pas pu être lu depuis le tableau de bord FLOW. + Les affectations des formulaires n\'ont pas pas pu être lu du tableau de bord Flow + Le formulaire %1$s n\'a pas pu être lu depuis le tableau de bord Flow. Rassurez vous que le formulaire est publié, et que les paramères d\' heure/date sont ok. Ce formulaire a été supprimer. @@ -261,5 +262,6 @@ Vous avez déjà la dernière version de l\'application. Une erreur est survenue en essayant d\'actualiser l\'application, veuillez réessayer plus tard. - Une erreur est survenue en essayant d\'actualiser l\'application, vous n\'etes pas connecté/e à internet. + Une erreur est survenue en essayant d\'actualiser l\'application, vous n\'êtes pas connecté/e à internet. + Localisation inconnue diff --git a/app/src/main/res/values-hi/strings.xml b/app/src/main/res/values-hi/strings.xml index 79aeeb0eb..3b277df27 100644 --- a/app/src/main/res/values-hi/strings.xml +++ b/app/src/main/res/values-hi/strings.xml @@ -1,6 +1,5 @@ - - खोज जारी है + सूक्ष्मता खोज जारी है तैयार @@ -30,8 +29,7 @@ सर्वे करते समय स्क्रीन को बंद नहीं होने देना | याद रहे की बैटरी की बचत के लिए, इस्तमाल न करते समय स्क्रीन को स्वयं बंद करदें (या ऐप्प से बाहर निकल जाएं ) ऐप्प के बारे में ऐप्प का वर्शन संख्या या प्रारूप जानने के लिए - Akvo FLOW ऐप्प के बारे में - Akvo FLOW एप्प , Akvo फाउंडेशन के द्वारा विकसित तथा २०१०-२०१४ तक कॉपीराइटेड है + Akvo Flow ऐप्प के बारे में GPS स्टेटस GPS स्टेटस ऐप्प को इस्तेमाल करने के लिए | इस ऐप्प का आपके फ़ोन या टैबलेट में होना आवश्यक है आपके उपकरण में GPS स्टेटस स्तापित नहीं है I इसे स्तापित करने के गूगल प्ले स्टोर इस्तमाल करैं I @@ -45,7 +43,6 @@ कार्य के मध्य चयन किये गए यूज़र को जारी रखें मोबाइल नेटवर्क का इस्तमाल करते हुए डेटा सिंक करें सर्वे की भाषाएँ - अपने स्तिथि की जानकारी बेजें बारकोड को स्कैन करें स्कैनर आपके फ़ोन अथवा टेबलेट पे उपलब्ध नहीं है I एंड्राइड मार्किट से बारकोड स्कैनर एप्प उपलब्ध करें I संख्या अति छोटी है I नुन्यतम संख्या डाले : @@ -127,7 +124,7 @@ ऐप की भाषा कृपया ऐप को पुनर्प्रारंभ करें - आप के पास कोई सर्वे नियुक्त नहीं हैI फ्लो, FLOW डैशबोर्ड में सर्वे नियुक्त किये जाते हैंI + आप के पास कोई सर्वे नियुक्त नहीं हैI फ्लो, Flow डैशबोर्ड में सर्वे नियुक्त किये जाते हैंI आपके पास कोई डेटा पॉइंट नहीं है । नया डेटा पॉइंट बनाने के लिए स्क्रीन के ऊपरी ओर \'+\' निशान को चुनियें । आपने कोई सर्वे नहीं चुना है । कृपया साइड मेनू से कोई सर्वे चुने । @@ -201,12 +198,12 @@ जमा हो चूका है निर्यातित - Akvo FLOW अपडेट - नया FLOW वर्शन उपलब्ध है | \"Update\" को चुनें व इसे डाउनलोड करें + Akvo Flow अपडेट + नया Flow वर्शन उपलब्ध है | \"Update\" को चुनें व इसे डाउनलोड करें अपडेट अभी नहीं - नया FLOW संस्करण उपलब्ध है. क्या आप इसे स्तापित करना चाहेंगे ? - FLOW update दोव्न्लोडिंग त्रुटिपूर्ण + नया Flow संस्करण उपलब्ध है. क्या आप इसे स्तापित करना चाहेंगे ? + Flow update दोव्न्लोडिंग त्रुटिपूर्ण समय व दिनांक आपके फ़ोन पे दिखाया जा रहा समय सही नहीं है। उसे सही करें और पुन:प्रयास करें। आपका दिखाया गया समय व दिनांक यह है @@ -214,10 +211,11 @@ फॉर्म डाउनलोड जारी फॉर्म सिंक करने में त्रुटि - फॉर्म असाइनमेंट्स FLOW dashboard पर पढ़े नहीं जा रहे हैं - यह फॉर्म %1$s FLOW dashboard पर पढ़ा नहीं जा सकता + फॉर्म असाइनमेंट्स Flow dashboard पर पढ़े नहीं जा रहे हैं + यह फॉर्म %1$s Flow dashboard पर पढ़ा नहीं जा सकता सुनिशिचित करें की फॉर्म प्रकाशित हो और आपका स्थानिक समय व दिनांक सही हो यह फॉर्म हटा दिया गया है + एरिया /क्षेत्र लाइन /रेखा @@ -249,4 +247,6 @@ पुन: प्रयास हटाएं इस स्थान को खली नहीं छोड़ें + + diff --git a/app/src/main/res/values-in/strings.xml b/app/src/main/res/values-in/strings.xml index 6e2defc11..8f0e30643 100644 --- a/app/src/main/res/values-in/strings.xml +++ b/app/src/main/res/values-in/strings.xml @@ -1,6 +1,5 @@ - - Akvo FLOW + Akurasi Sedang mencari... Siap digunakan @@ -31,8 +30,8 @@ Mencegah layar ponsel mati saat mode mengoleksi survei. Pastikan untuk mematikan layar secara manual (atau keluar dari aplikasi) saat tidak digunakan untuk menghemat daya. Tentang Informasi tentang versi. - Tentang - Aplikasi Akvo FLOW - Aplikasi Akvo FLOW app\nDikembangkan oleh: Akvo Foundation\nHak Cipta © 2010-2014 Stichting Akvo\nVersi + Tentang - Aplikasi Akvo Flow + Aplikasi Akvo Flow app\nDikembangkan oleh: Akvo Foundation\nHak Cipta © 2010-%1$s Stichting Akvo\nVersi %2$s GPS Status Nyalakan aplikasi GPS Status jika Anda telah menginstallnya dalam perangkat ini. Aplikasi GPS Status belum terpasang dalam perangkat ini. Mohon pasang melalui Google Play Store @@ -46,7 +45,6 @@ Tetapkan pengguna terakhir supaya aktif di waktu pengisian Sinkronisasi data melalui jaringan koneksi GSM Bahasa Survey - Kirim Suar Lokasi Pindai Kode Batang Aplikasi pemindai belum terpasang. Mohon pasang aplikasi Barcode Scanner melalui Android Market. Nilai terlalu kecil. Nilai paling kecil: @@ -86,7 +84,7 @@ Memuat data survei dari kartu Memori/SD card Selesai memuat data dari kartu memori/SD card Ada kesalahan dalam memproses file zip bootsrap - Kesalahan: Formulir bukan terkait dengan FLOW ini + Kesalahan: Formulir bukan terkait dengan Flow ini Riwayat Transmisi Dimulai: Selesai: @@ -118,7 +116,7 @@ Memuat... Mengisi respons awal Mengisi formulir dengan jawaban sebelumnya? - Membutuhkan penugasan dalam dasbor Akvo FLOW. + Membutuhkan penugasan dalam dasbor Akvo Flow. Sinkronisasi Data Mengunggah data. Mohon menunggu... @@ -128,7 +126,7 @@ Bahasa Aplikasi Mohon memulai ulang aplikasi - Anda belum mempunyai survei yang ditugaskan. Survei ditugaskan melalui dasbor FLOW + Anda belum mempunyai survei yang ditugaskan. Survei ditugaskan melalui dasbor Flow Anda belum mempunyai data. Klik simbol \'+\' diatas untuk menambahkan data. Anda belum memilih Survei. Buka menu samping untuk memilih Survei. @@ -202,12 +200,12 @@ Terkirim Terekspor - Pembaruan Akvo FLOW - Versi terbaru aplikasi FLOW telah tersedia. Klik \'Perbarui\' untuk mengunduh dan memasang versi baru. + Pembaruan Akvo Flow + Versi terbaru aplikasi Flow telah tersedia. Klik \'Perbarui\' untuk mengunduh dan memasang versi baru. Perbarui Lain waktu - Versi FLOW terbaru telah terunduh. Apakah Anda ingin memasangnya sekarang? - Ada kesalahan dalam mengunduh FLOW terbaru. + Versi Flow terbaru telah terunduh. Apakah Anda ingin memasangnya sekarang? + Ada kesalahan dalam mengunduh Flow terbaru. Tanggal dan Waktu Pengaturan waktu pada perangkat Anda tidak akurat! Atur ulang waktunya dan coba lagi. Tanggal dan waktu di perangkat Anda adalah: @@ -215,10 +213,11 @@ Mengunduh formulir Ada kesalahan dalam menyingkronisasi formulir - Penugasan formulir tidak dapat dibaca dari dasbor FLOW - Formulir %1$s tidak dapat dibaca oleh dasbor FLOW + Penugasan formulir tidak dapat dibaca dari dasbor Flow + Formulir %1$s tidak dapat dibaca oleh dasbor Flow Pastikan formulir ini sudah diterbitkan, dan pengaturan waktu Anda sudah benar Formulir tersebut telah dihapus + Area Garis @@ -252,4 +251,6 @@ Bagian ini tidak boleh kosong Laci terbuka Laci tertutup + + diff --git a/app/src/main/res/values-km/strings.xml b/app/src/main/res/values-km/strings.xml index 83722df86..1becd6109 100644 --- a/app/src/main/res/values-km/strings.xml +++ b/app/src/main/res/values-km/strings.xml @@ -1,6 +1,5 @@ - - Akvo FLOW + អាស៊ីស៊ី ស្វែងរក... រួចរាល់ @@ -31,8 +30,8 @@ បង្ការស្គ្រីនទូរស័ព្ទកុំឲ្យរលត់ពេលកំពុងបញ្ចូលទិន្នន័យអង្កេត។ ត្រូវបិទពន្លត់ស្គ្រីន (ឬចេញពី app) ពេលមិនប្រើ ដើម្បីចៀសវាងការស៊ីថ្មខ្លាំង អំពី ពត៌មានអំពីច្បាប់កំណែនៃអេក្រង់ - អំពី - AKVO Flow App - Akvo FLOW App\បង្កើតដោយ: Akvo Foundation\nCopyright © 2010-2014 Stichting Akvo\nVersion + អំពី - Akvo Flow App + Akvo Flow App\nបង្កើតដោយ: Akvo Foundation\nCopyright © 2010-%1$s Stichting Akvo\nVersion %2$s ស្ថានភាពជីភីអេស ដំណើរការស្ថានភាពជីភីអេស ប្រសិនបើអ្នកបានតម្លើងវាក្នុងឧបករណ៍នេះ ស្ថានភាពជីភីអេសពុំមានតម្លើងក្នុងឧបករណ៍នេះឡើយ។ សូមតម្លើងវាដោយចូលទៅក្នុង Google Play Store @@ -46,7 +45,6 @@ រក្សាទុកអ្នកប្រើប្រាស់ចុងក្រោយឲ្យចូលបានរហូត​រវាងវគ្គនិមួយៗ ទិន្នន័យដោយប្រើបណ្តាញទូរស័ព្ទ (3G) ភាសាប្រើក្នុងការអង្កេត - បញ្ជូនសញ្ញានាំផ្លូវទីតាំង ស្គែនសញ្ញានាំផ្លូវ ឧបករណ៍ស្គែនមិនបានតម្លើងឡើយ។ សូមតម្លើង Appសម្រាប់ស្កែនសញ្ញានាំផ្លូវនៅក្នុងផ្សារអាន់ដ្រូយ តម្លៃតូចជ្រុល។ តម្លៃអប្បបរមាៈ @@ -86,7 +84,7 @@ កំពុងផ្ទុកទិន្នន័យអង្កេតពី SD card ការផ្ទុកទិន្នន័យអង្កេតពី SD card បានធ្វើរួចរាល់ ដំណើរការ bootstrap ឯកសារzip​មានកំហុសឆ្គង - កំហុសឆ្គងៈ ទម្រង់មិនមែនជារបស់​ FLOW instance នេះទេ + កំហុសឆ្គងៈ ទម្រង់មិនមែនជារបស់​ Flow instance នេះទេ ប្រវតិ្តការបញ្ជូន បានចាប់ផ្តើមៈ បានបញ្ចប់ៈ @@ -118,7 +116,7 @@ កំពុងផ្ទុក... បំពេញចម្លើយជាមុន បំពេញទម្រង់ជាមុន ដោយប្រើចម្លើយលើកមុន? - ត្រូវការ ការចាត់ចែងកិច្ចការ នៅក្នុងដាសបូតរបស់ AKVO Flow + ត្រូវការ ការចាត់ចែងកិច្ចការ នៅក្នុងដាសបូតរបស់ Akvo Flow ការដាក់ទិន្នន័យឲ្យចេញដំណាលគ្នា កំពុងផ្ទុកទិន្នន័យចូលក្នុងឧបករណ៍។ សូមរង់ចាំ... @@ -202,8 +200,8 @@ បានបញ្ជូនចេញ បាននាំចេញ - ធ្វើបច្ចុប្បន្នភាព AKVO Flow - មានច្បាប់កំណែថ្មីរបស់ AKVO App។ ចូរចុច ​\'ធ្វើបច្ចុប្បន្នភាព\' ដើម្បីដោនឡូត និងតម្លើងច្បាប់កំណែថ្មី។ + ធ្វើបច្ចុប្បន្នភាព Akvo Flow + មានច្បាប់កំណែថ្មីរបស់ Akvo App។ ចូរចុច ​\'ធ្វើបច្ចុប្បន្នភាព\' ដើម្បីដោនឡូត និងតម្លើងច្បាប់កំណែថ្មី។ ធ្វើបច្ចុប្បន្នភាព មិនមែនពេលនេះទេ ច្បាប់កំណែថ្មីរបស់ Flow បានដោនឡូត។ តើអ្នកចង់តម្លើងវាឥឡូវឬទេ? @@ -219,6 +217,7 @@ ទម្រង់ %1$s មិនអាចអានចេញពីដាសបូតលំហូឡើយ ត្រូវប្រាកដថា ទម្រង់បានបោះពុម្ភ និងការកំណត់កាលបរិចេ្ឆទ និងពេលវេលាក្នុងស្រុករបស់អ្នក OK ទម្រង់នេះត្រូវបានលុបចេញ + តំបន់ ខ្សែ @@ -252,4 +251,6 @@ ចន្លោះនេះមិនអាចចំហចោលឡើយ ថត បើក ថត បិទ + + diff --git a/app/src/main/res/values-ne/strings.xml b/app/src/main/res/values-ne/strings.xml index 9e31cd3e7..a2414b221 100644 --- a/app/src/main/res/values-ne/strings.xml +++ b/app/src/main/res/values-ne/strings.xml @@ -1,6 +1,5 @@ - - एक्भो फ्लो + एक्युरेसी खोजी हुँदैछ... तयार @@ -31,8 +30,8 @@ सर्भेको उतर दिँदा उपकरणको स्क्रिनलाई बन्द हुन बाट रोक्दछ। ब्याट्री खपतलाई जोगाउन सर्भे प्रयोगमा नभएको बेला आफैले स्क्रिन बन्द गर्न (वा एप्पबाट बहिर निस्कन) नबिर्सनु होला। हाम्रो बारेमा भर्सनको बारेमा देखाउँछ - एक्भो फ्लो एप्पको बारेमा - एक्भो फ्लो एप्प विकास गर्नेः एक्भो फाउन्डेशन प्रतिलिपि अधिकारः © 2010-2014 स्टीचिङ्ग एकभो भर्सन + Akvo Flow एप्पको बारेमा + Akvo Flow एप्प विकास गर्नेः Akvo फाउन्डेशन प्रतिलिपि अधिकारः © 2010-%1$s स्टीचिङ्ग Akvo भर्सन %2$s जि पि एस् को स्थिति उपकरणमा GPS Status एप्प राख्नु भएको छ भने चालु गराउँदछ। GPS Status राखिएको छैन। कृपया गुगलको Play Store मा गएर डाउन्लोड ग रि राख्नु होला। @@ -46,7 +45,6 @@ सत्रहरुको बिचमा अन्तिमकै प्रयोगकर्तालाई सक्रिय राख्नुहोस्। मोबाइल नेटवर्क प्रयोग गरेर डाटा सिँक गर्नुहोस्। सर्भे भाषाहरु - स्थानको विवरण पठाउनुहोस्। बारकोड स्क्यान गर्नुहोस्। उपकरणमा स्क्यानर राखिएको छैन। कृपया Google Play Store बाट Barcode Scanner एप्प डाउन्लोड गरेर राख्नुहोला। मान धेरै सानो भयो। न्युनतम मान: @@ -86,7 +84,7 @@ एस डि कार्डबाट डाटा लोड हुँदैछ। एस डि कार्डबाट डाटा लोड भई सक्यो। बुट स्ट्र्याप zip फाइल प्रसारणमा त्रुटि - त्रुटि: फारम यस फ्लो instance को हैन। + त्रुटि: फारम यस Flow instance को हैन। डाटा प्रसारण हिस्ट्री शुरु भयो सकियो @@ -118,7 +116,7 @@ लोड हुँदैछ... पहिले नै भरिएका उतरहरु पहिलेको उतरहरुले फारम भर्न चाहनुहुन्छ? - एक्भो फ्लो ड्यास्बोर्डमा assignment हरु चाहियो। + Akvo Flow ड्यास्बोर्डमा assignment हरु चाहियो। डाटा सिँक्रोनाइजेसन डाटा अपलोड हुँदैछ, कृपया पर्खनुहोस्। @@ -202,12 +200,12 @@ बुझाईयो। पठाइयो। - एक्भो फ्लो अपडेट - फ्लो एप्पको नयाँ संस्करण उपलब्ध छ। नयाँ संस्करण डाउन्लोड गरेर चलाउनको लागि Update मा थिच्नुहोस्। + Akvo Flow अपडेट + Flow एप्पको नयाँ संस्करण उपलब्ध छ। नयाँ संस्करण डाउन्लोड गरेर चलाउनको लागि Update मा थिच्नुहोस्। अपडेट गर्नुहोस् अहिले हैन - नयाँ फ्लो भर्सन डाउन्लोड भएको छ। के तपाईं यसलाई ईन्स्टल गर्न चाहनुहुन्छ? - फ्लो अपडेट डाउन्लोड हुन सकेन + नयाँ Flow भर्सन डाउन्लोड भएको छ। के तपाईं यसलाई ईन्स्टल गर्न चाहनुहुन्छ? + Flow अपडेट डाउन्लोड हुन सकेन मिति र गते तपाईंको फोनको समय मिलेको छैन! कृपया फोनको घडी मिलाएर पुन: प्रयास गर्नु होला। तपाईंको फोनको मिति र समय @@ -215,10 +213,11 @@ फारम डाउन्लोड हुँदैछ फारम सिन्क हुन सकेन - फ्लो ड्यास्बोर्डबाट फारम असाइन्मेन्ट पढ्न सकिएन - फ्लो ड्यास्बोर्डबाट फारम %1$s पढ्न सकिएन + Flow ड्यास्बोर्डबाट फारम असाइन्मेन्ट पढ्न सकिएन + Flow ड्यास्बोर्डबाट फारम %1$s पढ्न सकिएन फारम पब्लिश भएको र स्थानिय मिति र समय ठीक भएको पक्का गर्नुहोस् यो फारम हटाइएको छ। + क्षेत्र रेखा @@ -252,4 +251,6 @@ यो कोठा खाली हुन सक्दैन। ड्रअर खुला छ ड्रअर बन्द छ + + diff --git a/app/src/main/res/values-pt/strings.xml b/app/src/main/res/values-pt/strings.xml index 804e29872..6c60fd3dc 100644 --- a/app/src/main/res/values-pt/strings.xml +++ b/app/src/main/res/values-pt/strings.xml @@ -1,6 +1,5 @@ - - Akvo FLOW + Precisão Procurando... Pronto @@ -18,7 +17,7 @@ Sem espaço para armazenamento Long Altura Submeter - O GPS deve estar ligado para o fazer isto. Seleccione OK para ir para\à página de definições. Seleccione Usar satélites do GPS e, em seguida, pressione o botão Voltar para retornar. Uma vez que o GPS esteja activo você pode clicar de novo em Verificar Localização Geográfica para obter a sua posição. + O GPS deve estar ligado para fazer isto. Seleccione OK para ir à página de definições. Seleccione Usar satélites do GPS e, em seguida, pressione o botão Voltar para retornar. Uma vez que o GPS esteja activo você pode clicar de novo em Verificar Localização Geográfica para obter a sua posição. Redimensionar imagens grandes Definições Sem usuários! @@ -33,8 +32,8 @@ Sem espaço para armazenamento Impedir que a tela se desligue enquanto no modo de preenchimento de dados no inquérito. Certifique-se de desligar manualmente a tela (ou sair do aplicativo) quando não estiver em uso para evitar gastar a bateria. Sobre Exibir informações sobre a versão. - Sobre o aplicativo Akvo FLOW - Aplicação Akvo FLOW \ Desenvolvido por: Akvo Foundation \ Copyright © 2010-2014 Stichting Akvo \ nVersion + Sobre o aplicativo Akvo Flow + Aplicação Akvo Flow\nDesenvolvido por: Akvo Foundation\nCopyright © 2010-%1$s Stichting Akvo\nVersão %2$s Estado do GPS Iniciar a aplicação do Estado do GPS se o tiver instalado no dispositivo. O aplicativo estado do GPS não está instalado neste dispositivo. Por favor, instale-o usando o Google Play Store. @@ -48,7 +47,6 @@ Sem espaço para armazenamento Manter o último usuário seleccionado com a sessão iniciada entre as sessões Sincronizar dados através da rede móvel Idiomas do inquérito - Enviar sinal de localização Digitalizar o código de barras Scanner não instalado. Por favor, instale a aplicação Barcode Scanner do Android Market. O valor é demasiado pequeno. Valor mínimo: @@ -88,7 +86,7 @@ Sem espaço para armazenamento Carregando dados do inquérito do cartão de memória Concluído o carregamento de dados do cartão de memória Erro no processamento do ficheiro de inicialização zip - Erro: O formulário não pertence a esta instância do FLOW + Erro: O formulário não pertence a esta instância do Flow Histórico de transmissão Iniciado: Terminado @@ -116,21 +114,24 @@ Sem espaço para armazenamento Sincronização de pontos de dados concluída Pontos de Dados %1$d sincronizados Falhou a sincronização de Pontos de Dados + Faça a sincronização novamente. Erro de rede Carregando... Preencher novamente as respostas Preencher antecipadamente o formulário com respostas anteriores? - É necessário o emparelhamento no painel do Akvo FLOW + É necessário o emparelhamento no painel do Akvo Flow Sincronização de Dados Carregando dados. Por favor, aguarde... Formulários sincronizados: %1$d - Falha:%2$d Formulários sincronizados:%1$d + Formulário eliminado + O formulário %1$s foi eliminado. Idioma da Aplicação Por favor, reinicie o aplicativo - Não existem inquéritos emparelhados. Os inquéritos são emparelhados no painel do FLOW + Não existem inquéritos emparelhados. Os inquéritos são emparelhados no painel do Flow Você ainda não tem pontos de dados. Clique em \'+\', no topo da tela para adicionar um novo ponto de dados Nenhum inquérito seleccionado. Abra o menu lateral para seleccionar um inquérito @@ -205,12 +206,12 @@ Sem espaço para armazenamento Submetido Exportado - Actualização do Akvo FLOW - Está disponível uma nova versão do aplicativo FLOW. Clique em \'Actualizar\' para descarregar e instalar a nova versão. + Actualização do Akvo Flow + Está disponível uma nova versão do aplicativo Flow. Clique em \'Actualizar\' para descarregar e instalar a nova versão. Actualizar Agora não - A nova versão do FLOW foi descarregada. Você deseja instalá-la agora? - Erro ao descarregar a actualização do FLOW. + A nova versão do Flow foi descarregada. Você deseja instalá-la agora? + Erro ao descarregar a actualização do Flow. Data e Hora A hora do seu telefone não está correcta! Acerte o seu relógio e tente de novo. A data do e hora do seu telefone são: @@ -218,8 +219,8 @@ Sem espaço para armazenamento Descarregando formulários Erro de sincronização de formulários - O emparelhamento de formulários não pode ser lido a partir do painel do FLOW. - O Formulário %1$s não pode ser lido a partir do painel do FLOW. + O emparelhamento de formulários não pode ser lido a partir do painel do Flow. + O Formulário %1$s não pode ser lido a partir do painel do Flow. Certifique-se de que o formulário foi publicado e de que as suas definições de data / hora locais estão correctas Este formulário foi eliminado. @@ -265,4 +266,5 @@ Sem espaço para armazenamento Você já tem a versão mais recente do aplicativo. Houve um erro ao tentar atualizar o aplicativo, por favor, tente novamente. Houve um erro ao tentar atualizar o aplicativo, você não está conectado à Internet. + Localização desconhecida diff --git a/app/src/main/res/values-vi/strings.xml b/app/src/main/res/values-vi/strings.xml index 161646129..5b0a1e8e7 100644 --- a/app/src/main/res/values-vi/strings.xml +++ b/app/src/main/res/values-vi/strings.xml @@ -1,6 +1,5 @@ - - Phần mềm Akvo + Độ chính xác Đang tìm kiếm Sẵn sàng @@ -32,7 +31,6 @@ Phiên bản Hiển thị thông tin của phiên bản Giới thiệu ứng dụng Akvo Flow - Akvo FLOW app\nDeveloped By: Akvo Foundation\nCopyright © 2010-2014 Stichting Akvo\nVersion Trạng thái định vị tọa độ Khởi động ứng dụng định vị tọa độ nếu bạn đã cài đặt nó trên thiết bị này Ứng dụng định vị tọa độ chưa được cài đặt trên thiết bị này. Vui lòng cài đặt nó bằng sử dụng kho dữ liệu của Google Play @@ -46,7 +44,6 @@ Giữ người sử dụng được chọn cuối cùng ở chế độ đăng nhập trong những lần tiếp theo Đồng bộ hóa dữ liệu qua mạng di động Những ngôn ngữ của đợt khảo sát - Gửi những cột mốc vị trí Quét mã vạch Máy quét chưa được cài đặt. Vui lòng cài đặt ứng dụng Quét mã vạch của Anroid Market Giá trị quá nhỏ. Giá trị nhỏ nhất là: @@ -118,7 +115,7 @@ Đang tải... Những câu trả lời trước đây Biểu mẫu với những câu trả lời trước đây? - Nhiệm vụ được yêu cầu trong bảng điều khiển Akvo FLOW + Nhiệm vụ được yêu cầu trong bảng điều khiển Akvo Flow Đồng bộ hóa dữ liệu Đang tải dữ liệu lên. Vui lòng đợi… @@ -216,9 +213,10 @@ Đang tải các biểu mẫu Lỗi khi đồng bộ hóa biểu mẫu Không thể hiểu được việc chỉ định biểu mẫu từ bảng điều khiển Flow - Phần trăm %1$d biểu mẫu không được hiểu từ bảng điều khiển Flow + Phần trăm %1$s biểu mẫu không được hiểu từ bảng điều khiển Flow Chắc chắn rằng biểu mẫu này đã được công bố, và ngày tháng/thời gian của khu vực của bạn là ok Biểu mẫu này đã bị xóa + Vùng Dòng @@ -252,4 +250,6 @@ Trường dữ liệu này không thể bỏ trống Drawer open Drawer close + + diff --git a/app/src/main/res/values/attrs.xml b/app/src/main/res/values/attrs.xml index 9302dfac2..3461920c6 100644 --- a/app/src/main/res/values/attrs.xml +++ b/app/src/main/res/values/attrs.xml @@ -2,11 +2,6 @@ - - - - - diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 7b4ae59af..e9ea3e30c 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -5,6 +5,7 @@ #000000 #8f8f8f + #20000000 #e17e26 #80e17e26 @@ -18,6 +19,9 @@ #808285 #79736e #000000 + #5094918e + #94918e + #85827f #fffef2e7 @@ -26,10 +30,6 @@ @color/black_main @color/black_disabled - @color/orange_main - @color/orange_disabled - @color/blue_main - @color/blue_disabled @color/orange_main @@ -48,6 +48,8 @@ @color/button_grey_dark_selected @color/white + #FF0000 + #ff33b5e5 #cec6bc diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 46c739f37..34941056c 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -6,4 +6,7 @@ 16dp 16dp + 18sp + 22sp + diff --git a/app/src/main/res/values/donottranslate.xml b/app/src/main/res/values/donottranslate.xml index ee33c65b8..007f98699 100644 --- a/app/src/main/res/values/donottranslate.xml +++ b/app/src/main/res/values/donottranslate.xml @@ -2,6 +2,6 @@ fieldsurvey.ACTION_SURVEYS_SYNC - fieldsurvey.ACTION_LOCALES_SYNC - fieldsurvey.ACTION_DATA_SYNC + AIzaSyBPyguWZrQi2xcqs49WajwQ6FYeSHpaEFQ + Akvo Flow \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index efae0e232..05043f879 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,6 +1,5 @@ - - Akvo FLOW + Acc Searching... Ready @@ -16,7 +15,7 @@ Lon Height Submit - GPS must be enabled to do this. Select OK to go to\tthe settings page. Select Use GPS Satellites and then press the back button to return. Once GPS is enabled you can click Check Geo Location again to get your position. + GPS must be enabled to do this. Select OK to go to the settings page. Select Use GPS Satellites and then press the back button to return. Once GPS is enabled you can click Check Geo Location again to get your position. Resize large images Settings No Users! @@ -31,8 +30,8 @@ Prevents the screen from turning off while in survey entry mode. Be sure to manually shut the screen off (or exit the app) when not in use to avoid draining the battery. About Displays version information. - About - Akvo FLOW app - Akvo FLOW app\nDeveloped By: Akvo Foundation\nCopyright © 2010-2014 Stichting Akvo\nVersion + About - Akvo Flow app + Akvo Flow app\nDeveloped By: Akvo Foundation\nCopyright © 2010-%1$s Stichting Akvo\nVersion %2$s GPS Status Launches the GPS Status application if you have it installed on this device. GPS Status is not installed on this device. Please install it using the Google Play Store. @@ -46,7 +45,6 @@ Keep the last selected user logged-in between sessions Sync data over mobile network Survey Languages - Send Location Beacons Scan Barcode Scanner not installed. Please install the Barcode Scanner app from the Android Market. Value is too small. Minimum value: @@ -86,7 +84,7 @@ Loading survey data from SD card Done loading data from SD card Error processing bootstrap zip file - Error: Form doesn\'t belong to this FLOW instance + Error: Form doesn\'t belong to this Flow instance Transmission History Started: Finished: @@ -114,21 +112,24 @@ Data Points Sync finished Synced %1$d Data Points Data Points Sync failed + Please sync again. Network Error Loading... Pre-fill Responses Pre-fill form with previous answers? - Assignment in Akvo FLOW dashboard required + Flow dashboard assignment required Data Synchronization Uploading data. Please, wait... Synced Forms: %1$d - Failed: %2$d Synced Forms: %1$d + Form deleted + Form %1$s has been deleted App Language Please, restart the app - You have no surveys assigned. Surveys are assigned in the FLOW dashboard + You have no surveys assigned. Surveys are assigned in the Flow dashboard You have no data points yet. Click \'+\' at the top of your screen to add a new data point You have no Survey selected. Open the side menu to select one. @@ -202,12 +203,12 @@ Submitted Exported - Akvo FLOW Update - A new version of the FLOW app is available. Click \'Update\' to download and install the new version. + Akvo Flow Update + A new version of the Flow app is available. Click \'Update\' to download and install the new version. Update Not now - New FLOW version is downloaded. Do you want to install it now? - Error downloading FLOW update. + New Flow version is downloaded. Do you want to install it now? + Error downloading Flow update. Date and Time Your phone time is inaccurate! Adjust your clock and try again. Your phone date and time is: @@ -215,8 +216,8 @@ Downloading forms Error syncing forms - Form assignments could not be read from the FLOW dashboard. - Form %1$s could not be read from FLOW dashboard. + Form assignments could not be read from the Flow dashboard. + Form %1$s could not be read from Flow dashboard. Ensure the form is published, and your local date/time settings are OK. This form has been deleted. @@ -264,4 +265,7 @@ You are running the latest version of the app. There was an error when updating the app. Please try again. There was an error when updating the app. You are not connected to the internet. + + Unknown Location + diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index cb04b75cb..de4475c29 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -2,7 +2,6 @@ - + + - @@ -50,7 +54,20 @@ @color/button_tertiary_text - - + + + \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index bc62150f2..79cc1cf2e 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -2,11 +2,12 @@ - -