/* * 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/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 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 <http://www.gnu.org/licenses/>. * */ package org.akvo.flow.activity; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.Bundle; import android.support.annotation.NonNull; import android.support.design.widget.TabLayout; import android.support.v4.view.ViewPager; import android.support.v7.app.AlertDialog; import android.text.TextUtils; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.widget.AdapterView; import android.widget.ListView; import org.akvo.flow.R; import org.akvo.flow.data.SurveyLanguagesDataSource; import org.akvo.flow.data.dao.SurveyDao; import org.akvo.flow.data.database.SurveyDbAdapter; import org.akvo.flow.data.database.SurveyDbAdapter.SurveyedLocaleMeta; import org.akvo.flow.data.database.SurveyInstanceStatus; import org.akvo.flow.data.database.SurveyLanguagesDbDataSource; import org.akvo.flow.data.preference.Prefs; import org.akvo.flow.domain.QuestionGroup; import org.akvo.flow.domain.QuestionResponse; import org.akvo.flow.domain.Survey; import org.akvo.flow.domain.SurveyGroup; import org.akvo.flow.event.QuestionInteractionEvent; import org.akvo.flow.event.QuestionInteractionListener; import org.akvo.flow.event.SurveyListener; import org.akvo.flow.ui.Navigator; import org.akvo.flow.ui.adapter.LanguageAdapter; import org.akvo.flow.ui.adapter.SurveyTabAdapter; import org.akvo.flow.ui.model.Language; import org.akvo.flow.ui.model.LanguageMapper; import org.akvo.flow.ui.view.QuestionView; import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.FileUtil; import org.akvo.flow.util.FileUtil.FileType; import org.akvo.flow.util.MediaFileHelper; import org.akvo.flow.util.StorageHelper; 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.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import timber.log.Timber; import static org.akvo.flow.util.ViewUtil.showConfirmDialog; public class FormActivity extends BackActivity implements SurveyListener, QuestionInteractionListener { private final Navigator navigator = new Navigator(); private final StorageHelper storageHelper = new StorageHelper(); private MediaFileHelper mediaFileHelper; /** * When a request is done to perform photo, video, barcode scan, etc we store * the question id, so we can notify later the result of such operation. */ private String mRequestQuestionId; private ViewPager mPager; private SurveyTabAdapter mAdapter; private boolean mReadOnly;//flag to represent whether the Survey can be edited or not private long mSurveyInstanceId; private long mSessionStartTime; private String mRecordId; private SurveyGroup mSurveyGroup; private Survey mSurvey; private SurveyDbAdapter mDatabase; private SurveyLanguagesDataSource surveyLanguagesDataSource; private Prefs prefs; private String[] mLanguages; private LanguageMapper languageMapper; private Map<String, QuestionResponse> mQuestionResponses;// QuestionId - QuestionResponse private String surveyId; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.form_activity); // Read all the params. Note that the survey instance id is now mandatory 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); mRecordId = getIntent().getStringExtra(ConstantUtil.SURVEYED_LOCALE_ID); mQuestionResponses = new HashMap<>(); mDatabase = new SurveyDbAdapter(this); mDatabase.open(); surveyLanguagesDataSource = new SurveyLanguagesDbDataSource(getApplicationContext()); prefs = new Prefs(getApplicationContext()); languageMapper = new LanguageMapper(getApplicationContext()); mediaFileHelper = new MediaFileHelper(this); //TODO: move all loading to worker thread loadSurvey(surveyId); loadLanguages(); if (mSurvey == null) { Timber.e("mSurvey is null. Finishing the Activity..."); finish(); } setupToolBar(); // Set the survey name as Activity title getSupportActionBar().setTitle(mSurvey.getName()); getSupportActionBar().setSubtitle("v " + getVersion()); mPager = (ViewPager) findViewById(R.id.pager); TabLayout tabLayout = (TabLayout) findViewById(R.id.tabs); tabLayout.setupWithViewPager(mPager); mAdapter = new SurveyTabAdapter(this, mPager, this, this); mPager.setAdapter(mAdapter); // Initialize new survey or load previous responses Map<String, QuestionResponse> responses = mDatabase.getResponses(mSurveyInstanceId); if (!responses.isEmpty()) { displayResponses(responses); } spaceLeftOnCard(); } /** * Display prefill option dialog, if applies. This feature is only available * for monitored groups, when a new survey instance is created, allowing users * to 'clone' responses from the previous response. */ private void displayPrefillDialog() { final Long lastSurveyInstance = mDatabase.getLastSurveyInstance(mRecordId, mSurvey.getId()); if (lastSurveyInstance != null) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.prefill_title); builder.setMessage(R.string.prefill_text); builder.setPositiveButton(R.string.yes, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { prefillSurvey(lastSurveyInstance); dialog.dismiss(); } }); builder.setNegativeButton(R.string.no, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); } }); builder.show(); } } private void prefillSurvey(long prefillSurveyInstance) { Map<String, QuestionResponse> responses = mDatabase.getResponses(prefillSurveyInstance); for (QuestionResponse response : responses.values()) { // Adapt(clone) responses for the current survey instance: // Get rid of its Id and update the SurveyInstance Id response.setId(null); response.setRespondentId(mSurveyInstanceId); } displayResponses(responses); } private void loadSurvey(String surveyId) { Survey surveyMeta = mDatabase.getSurvey(surveyId); InputStream in = null; try { // load from file File file = new File(FileUtil.getFilesDir(FileType.FORMS), surveyMeta.getFileName()); in = new FileInputStream(file); mSurvey = SurveyDao.loadSurvey(surveyMeta, in); mSurvey.setId(surveyId); } catch (FileNotFoundException e) { Timber.e(e, "Could not load survey xml file"); } finally { if (in != null) { try { in.close(); } catch (IOException e) { //EMPTY } } } } private double getVersion() { double version = 0.0; Cursor c = mDatabase.getFormInstance(mSurveyInstanceId); if (c.moveToFirst()) { version = c.getDouble(SurveyDbAdapter.FormInstanceQuery.VERSION); } c.close(); if (version == 0.0) { version = mSurvey.getVersion();// Default to current value } return version; } /** * Load state for the current survey instance */ private void loadResponses() { Map<String, QuestionResponse> responses = mDatabase.getResponses(mSurveyInstanceId); displayResponses(responses); } /** * Load state with the provided responses map */ private void displayResponses(Map<String, QuestionResponse> responses) { mQuestionResponses = responses; mAdapter.reset();// Propagate the change } /** * Handle survey session duration. Only 'active' survey time will be consider, that is, * the time range between onResume() and onPause() callbacks. Survey submission will also * stop the recording. This feature is only used if the mReadOnly flag is not active. * * @param start true if the call is to start recording, false to stop and save the duration. */ private void recordDuration(boolean start) { if (mReadOnly) { return; } final long time = System.currentTimeMillis(); if (start) { mSessionStartTime = time; } else { mDatabase.addSurveyDuration(mSurveyInstanceId, time - mSessionStartTime); // Restart the current session timer, in case we receive subsequent calls // to record the time, w/o setting up the timer first. mSessionStartTime = time; } } private void saveState() { if (!mReadOnly) { mDatabase.updateSurveyStatus(mSurveyInstanceId, SurveyInstanceStatus.SAVED); mDatabase.updateRecordModifiedDate(mRecordId, System.currentTimeMillis()); // Record meta-data, if applies if (!mSurveyGroup.isMonitored() || mSurvey.getId().equals(mSurveyGroup.getRegisterSurveyId())) { saveRecordMetaData(); } } } private void saveRecordMetaData() { // META_NAME StringBuilder builder = new StringBuilder(); List<String> localeNameQuestions = mSurvey.getLocaleNameQuestions(); // Check the responses given to these questions (marked as name) // and concatenate them so it becomes the Locale name. if (!localeNameQuestions.isEmpty()) { boolean first = true; for (String questionId : localeNameQuestions) { QuestionResponse questionResponse = mDatabase .getResponse(mSurveyInstanceId, questionId); String answer = questionResponse != null ? questionResponse.getDatapointNameValue() : null; if (!TextUtils.isEmpty(answer)) { if (!first) { builder.append(" - "); } builder.append(answer); first = false; } } // Make sure the value is not larger than 500 chars builder.setLength(Math.min(builder.length(), 500)); mDatabase.updateSurveyedLocale(mSurveyInstanceId, builder.toString(), SurveyedLocaleMeta.NAME); } // META_GEO String localeGeoQuestion = mSurvey.getLocaleGeoQuestion(); if (localeGeoQuestion != null) { QuestionResponse response = mDatabase.getResponse(mSurveyInstanceId, localeGeoQuestion); if (response != null) { mDatabase.updateSurveyedLocale(mSurveyInstanceId, response.getValue(), SurveyedLocaleMeta.GEOLOCATION); } } } @Override protected void onResume() { super.onResume(); mAdapter.onResume(); recordDuration(true);// Keep track of this session's duration. mPager.setKeepScreenOn( prefs.getBoolean(Prefs.KEY_SCREEN_ON, Prefs.DEFAULT_VALUE_SCREEN_ON)); } @Override public void onPause() { super.onPause(); mPager.setKeepScreenOn(false); mAdapter.onPause(); recordDuration(false); saveState(); } @Override public void onDestroy() { super.onDestroy(); mAdapter.onDestroy(); mDatabase.close(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.form_activity, menu); SubMenu subMenu = menu.findItem(R.id.more_submenu).getSubMenu(); if (isReadOnly()) { subMenu.removeItem(R.id.clear); subMenu.removeItem(R.id.prefill); } else { subMenu.removeItem(R.id.view_map); subMenu.removeItem(R.id.transmission); if (!mSurveyGroup.isMonitored() || mDatabase.getLastSurveyInstance(mRecordId, mSurvey.getId()) == null) { subMenu.removeItem(R.id.prefill); } } return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.edit_lang: displayLanguagesDialog(); return true; case R.id.clear: clearSurvey(); return true; case R.id.prefill: displayPrefillDialog(); return true; case R.id.view_map: navigator.navigateToMapActivity(this, mRecordId); return true; case R.id.transmission: navigator.navigateToTransmissionActivity(this, mSurveyInstanceId); return true; } return super.onOptionsItemSelected(item); } private void clearSurvey() { showConfirmDialog(R.string.cleartitle, R.string.cleardesc, this, true, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { mDatabase.deleteResponses(String.valueOf(mSurveyInstanceId)); loadResponses(); spaceLeftOnCard(); } }); } private void displayLanguagesDialog() { final ListView listView = createLanguagesList(); AlertDialog alertDialog = new AlertDialog.Builder(this) .setTitle(R.string.surveylanglabel) .setView(listView) .setPositiveButton(R.string.okbutton, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (dialog != null) { dialog.dismiss(); } useSelectedLanguages((LanguageAdapter) listView.getAdapter()); } }).create(); alertDialog.show(); } private void useSelectedLanguages(LanguageAdapter languageAdapter) { Set<String> selectedLanguages = languageAdapter.getSelectedLanguages(); if (selectedLanguages != null && selectedLanguages.size() > 0) { saveLanguages(selectedLanguages); } else { displayError(); } } private void displayError() { ViewUtil.showConfirmDialog(R.string.langmandatorytitle, R.string.langmandatorytext, this, false, new DialogInterface.OnClickListener() { @Override public void onClick( DialogInterface dialog, int which) { if (dialog != null) { dialog.dismiss(); } displayLanguagesDialog(); } }); } private void saveLanguages(Set<String> selectedLanguages) { surveyLanguagesDataSource.saveLanguagePreferences(mSurveyGroup.getId(), selectedLanguages); loadLanguages(); mAdapter.notifyOptionsChanged(); } @NonNull private ListView createLanguagesList() { List<Language> languages = languageMapper .transform(mLanguages, mSurvey.getAvailableLanguageCodes()); final LanguageAdapter languageAdapter = new LanguageAdapter(this, languages); final ListView listView = (ListView) LayoutInflater.from(this) .inflate(R.layout.languages_list, null); listView.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); listView.setAdapter(languageAdapter); listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { languageAdapter.updateSelected(position); } }); return listView; } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (mRequestQuestionId == null || resultCode != RESULT_OK) { mRequestQuestionId = null; return;// Move along, nothing to see here } switch (requestCode) { case ConstantUtil.PHOTO_ACTIVITY_REQUEST: String imageAbsolutePath = mediaFileHelper.getImageFilePath(prefs .getInt(Prefs.KEY_MAX_IMG_SIZE, Prefs.DEFAULT_VALUE_IMAGE_SIZE)); onMediaAcquired(imageAbsolutePath); break; case ConstantUtil.VIDEO_ACTIVITY_REQUEST: String videoAbsolutePath = mediaFileHelper.getVideoFilePath(); onMediaAcquired(videoAbsolutePath); break; case ConstantUtil.EXTERNAL_SOURCE_REQUEST: case ConstantUtil.CADDISFLY_REQUEST: case ConstantUtil.SCAN_ACTIVITY_REQUEST: case ConstantUtil.PLOTTING_REQUEST: case ConstantUtil.SIGNATURE_REQUEST: default: mAdapter.onQuestionComplete(mRequestQuestionId, data.getExtras()); break; } mRequestQuestionId = null;// Reset the tmp reference } private void onMediaAcquired(String absolutePath) { Bundle mediaData = new Bundle(); mediaData.putString(ConstantUtil.MEDIA_FILE_KEY, absolutePath); mAdapter.onQuestionComplete(mRequestQuestionId, mediaData); } @NonNull private String getDefaultLang() { //TODO: check if survey is null? return mSurvey.getDefaultLanguageCode(); } //TODO: use loader private void loadLanguages() { Set<String> languagePreferences = surveyLanguagesDataSource .getLanguagePreferences(mSurveyGroup.getId()); mLanguages = languagePreferences.toArray(new String[languagePreferences.size()]); } @Override public List<QuestionGroup> getQuestionGroups() { return mSurvey.getQuestionGroups(); } @Override public String getDefaultLanguage() { return getDefaultLang(); } @Override public String[] getLanguages() { return mLanguages; } @Override public boolean isReadOnly() { return mReadOnly; } @Override public void onSurveySubmit() { recordDuration(false); saveState(); // if we have no missing responses, submit the survey mDatabase.updateSurveyStatus(mSurveyInstanceId, SurveyInstanceStatus.SUBMITTED); // Make the current survey immutable mReadOnly = true; // send a broadcast message indicating new data is available Intent i = new Intent(ConstantUtil.DATA_AVAILABLE_INTENT); sendBroadcast(i); showConfirmDialog(R.string.submitcompletetitle, R.string.submitcompletetext, this, false, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { if (dialog != null) { setResult(RESULT_OK); finish(); } } }); } @Override public void nextTab() { mPager.setCurrentItem(mPager.getCurrentItem() + 1, true); } @Override public void openQuestion(String questionId) { int tab = mAdapter.displayQuestion(questionId); if (tab != -1) { mPager.setCurrentItem(tab, true); } } @Override public Map<String, QuestionResponse> getResponses() { return mQuestionResponses; } @Override public void deleteResponse(String questionId) { mQuestionResponses.remove(questionId); mDatabase.deleteResponse(mSurveyInstanceId, questionId); } @Override public QuestionView getQuestionView(String questionId) { return mAdapter.getQuestionView(questionId); } @Override public String getDatapointId() { return mRecordId; } @Override public String getFormId() { return mSurvey.getId(); } /** * event handler that can be used to handle events fired by individual * questions at the Activity level. Because we can't launch the photo * activity from a view (we need to launch it from the activity), the photo * question view fires a QuestionInteractionEvent (to which this activity * listens). When we get the event, we can then spawn the camera activity. * Currently, this method supports handing TAKE_PHOTO_EVENT and * VIDEO_TIP_EVENT types */ public void onQuestionInteraction(QuestionInteractionEvent event) { if (QuestionInteractionEvent.TAKE_PHOTO_EVENT.equals(event.getEventType())) { navigateToTakePhoto(event); } else if (QuestionInteractionEvent.TAKE_VIDEO_EVENT.equals(event.getEventType())) { navigateToTakeVideo(event); } else if (QuestionInteractionEvent.SCAN_BARCODE_EVENT.equals(event.getEventType())) { navigateToBarcodeScanner(event); } else if (QuestionInteractionEvent.QUESTION_CLEAR_EVENT.equals(event.getEventType())) { clearQuestion(event); } else if (QuestionInteractionEvent.QUESTION_ANSWER_EVENT.equals(event.getEventType())) { storeAnswer(event); } else if (QuestionInteractionEvent.EXTERNAL_SOURCE_EVENT.equals(event.getEventType())) { navigateToExternalSource(event); } else if (QuestionInteractionEvent.CADDISFLY.equals(event.getEventType())) { navigateToCaddisfly(event); } else if (QuestionInteractionEvent.PLOTTING_EVENT.equals(event.getEventType())) { navigateToGeoShapeActivity(event); } else if (QuestionInteractionEvent.ADD_SIGNATURE_EVENT.equals(event.getEventType())) { navigateToSignatureActivity(event); } } private void navigateToSignatureActivity(QuestionInteractionEvent event) { mRequestQuestionId = event.getSource().getQuestion().getId(); navigator.navigateToSignatureActivity(this); } private void navigateToGeoShapeActivity(QuestionInteractionEvent event) { mRequestQuestionId = event.getSource().getQuestion().getId(); navigator.navigateToGeoShapeActivity(this, event.getData()); } private void navigateToCaddisfly(QuestionInteractionEvent event) { mRequestQuestionId = event.getSource().getQuestion().getId(); navigator.navigateToCaddisfly(this, event.getData(), getString(R.string.caddisfly_test)); } private void navigateToExternalSource(QuestionInteractionEvent event) { mRequestQuestionId = event.getSource().getQuestion().getId(); navigator.navigateToExternalSource(this, event.getData(), getString(R.string.use_external_source)); } private void storeAnswer(QuestionInteractionEvent event) { String questionId = event.getSource().getQuestion().getId(); QuestionResponse response = event.getSource().getResponse(); // Store the response if it contains a value. Otherwise, delete it if (response != null && response.hasValue()) { mQuestionResponses.put(questionId, response); response.setRespondentId(mSurveyInstanceId); mDatabase.createOrUpdateSurveyResponse(response); } else { event.getSource().setResponse(null, true);// Invalidate previous response deleteResponse(questionId); } } private void clearQuestion(QuestionInteractionEvent event) { String questionId = event.getSource().getQuestion().getId(); deleteResponse(questionId); } private void navigateToBarcodeScanner(QuestionInteractionEvent event) { recordSourceId(event); navigator.navigateToBarcodeScanner(this); } private void recordSourceId(QuestionInteractionEvent event) { if (event.getSource() != null) { mRequestQuestionId = event.getSource().getQuestion().getId(); } else { Timber.e("Question source was null in the event"); } } private void navigateToTakeVideo(QuestionInteractionEvent event) { recordSourceId(event); navigator.navigateToTakeVideo(this, getVideoFileUri()); } private Uri getVideoFileUri() { return Uri.fromFile(mediaFileHelper.getVideoTmpFile()); } private void navigateToTakePhoto(QuestionInteractionEvent event) { recordSourceId(event); navigator.navigateToTakePhoto(this, getImageFileUri()); } private Uri getImageFileUri() { return Uri.fromFile(mediaFileHelper.getImageTmpFile()); } /* * Check SD card space. Warn by dialog popup if it is getting low. Return to * home screen if completely full. */ private void spaceLeftOnCard() { long megaAvailable = storageHelper.getExternalStorageAvailableSpace(); // keep track of changes // assume we had space before long lastMegaAvailable = prefs .getLong(Prefs.KEY_SPACE_AVAILABLE, Prefs.DEF_VALUE_SPACE_AVAILABLE); prefs.setLong(Prefs.KEY_SPACE_AVAILABLE, megaAvailable); if (megaAvailable <= 0L) {// All out, OR media not mounted // Bounce user showConfirmDialog(R.string.nocardspacetitle, R.string.nocardspacedialog, this, false, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (dialog != null) { dialog.dismiss(); } finish(); } } ); return; } // just issue a warning if we just descended to or past a number on the list if (megaAvailable < lastMegaAvailable) { for (long l = megaAvailable; l < lastMegaAvailable; l++) { if (ConstantUtil.SPACE_WARNING_MB_LEVELS.contains(Long.toString(l))) { // display how much space is left //TODO: replace "%%%" by "%s" and use String formatting String message = getResources().getString(R.string.lowcardspacedialog); message = message.replace("%%%", Long.toString(megaAvailable)); showConfirmDialog(R.string.lowcardspacetitle, message, this, false, new DialogInterface.OnClickListener() { @Override public void onClick(DialogInterface dialog, int which) { if (dialog != null) { dialog.dismiss(); } } }, null); return; // only one warning per survey, even of we passed >1 limit } } } } }