Java tutorial
/* * Copyright (C) 2014 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 <http://www.gnu.org/licenses/agpl.html>. */ package org.akvo.flow.activity; import android.app.AlertDialog; import android.content.ActivityNotFoundException; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.net.Uri; import android.os.Bundle; import android.os.Environment; import android.os.StatFs; import android.support.v4.view.ViewPager; import android.support.v7.app.ActionBarActivity; import android.text.TextUtils; import android.util.Log; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.widget.Toast; import org.akvo.flow.R; import org.akvo.flow.dao.SurveyDao; import org.akvo.flow.dao.SurveyDbAdapter; import org.akvo.flow.dao.SurveyDbAdapter.SurveyInstanceStatus; import org.akvo.flow.dao.SurveyDbAdapter.SurveyedLocaleMeta; 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.adapter.SurveyTabAdapter; import org.akvo.flow.util.ConstantUtil; import org.akvo.flow.util.FileUtil; import org.akvo.flow.util.FileUtil.FileType; import org.akvo.flow.util.ImageUtil; import org.akvo.flow.util.LangsPreferenceData; import org.akvo.flow.util.LangsPreferenceUtil; import org.akvo.flow.util.PlatformUtil; 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; public class SurveyActivity extends ActionBarActivity implements SurveyListener, QuestionInteractionListener { private static final String TAG = SurveyActivity.class.getSimpleName(); 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 MENU_PREFILL = 101; private static final String TEMP_PHOTO_NAME_PREFIX = "image"; private static final String TEMP_VIDEO_NAME_PREFIX = "video"; private static final String IMAGE_SUFFIX = ".jpg"; private static final String VIDEO_SUFFIX = ".mp4"; /** * 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 String[] mLanguages; private Map<String, QuestionResponse> mQuestionResponses;// QuestionId - QuestionResponse @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.survey_activity); // Read all the params. Note that the survey instance id is now mandatory 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); mRecordId = getIntent().getStringExtra(ConstantUtil.SURVEYED_LOCALE_ID); mQuestionResponses = new HashMap<String, QuestionResponse>(); mDatabase = new SurveyDbAdapter(this); mDatabase.open(); loadSurvey(surveyId);// Load Survey. This task would be better off if executed in a worker thread loadLanguages(); if (mSurvey == null) { Log.e(TAG, "mSurvey is null. Finishing the Activity..."); finish(); } // Set the survey name as Activity title setTitle(mSurvey.getName()); 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<String, QuestionResponse> responses = mDatabase.getResponses(mSurveyInstanceId); if (!responses.isEmpty()) { loadState(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); } loadState(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) { Log.e(TAG, "Could not load survey xml file"); } finally { if (in != null) { try { in.close(); } catch (IOException e) { } } } } /** * Load state for the current survey instance */ private void loadState() { Map<String, QuestionResponse> responses = mDatabase.getResponses(mSurveyInstanceId); loadState(responses); } /** * Load state with the provided responses map */ private void loadState(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 (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.size() > 0) { for (int i = 0; i < localeNameQuestions.size(); i++) { QuestionResponse questionResponse = mDatabase.getResponse(mSurveyInstanceId, localeNameQuestions.get(i)); String answer = questionResponse != null ? questionResponse.getValue() : null; if (!TextUtils.isEmpty(answer)) { if (i > 0) { builder.append(" - "); } builder.append(answer); } } 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(); recordDuration(true);// Keep track of this session's duration. if (Boolean.valueOf(mDatabase.getPreference(ConstantUtil.SCREEN_ON_KEY))) { mPager.setKeepScreenOn(true); } } @Override public void onPause() { super.onPause(); mPager.setKeepScreenOn(false); mAdapter.onPause(); recordDuration(false); saveState(); } @Override public void onDestroy() { super.onDestroy(); mDatabase.close(); } @Override public boolean onCreateOptionsMenu(Menu menu) { getMenuInflater().inflate(R.menu.survey_activity, menu); SubMenu subMenu = menu.findItem(R.id.more_submenu).getSubMenu(); if (isReadOnly()) { subMenu.removeItem(R.id.clear); } else if (mSurveyGroup.isMonitored()) { // Add 'pre-fill' option, if applies if (mDatabase.getLastSurveyInstance(mRecordId, mSurvey.getId()) != null) { subMenu.add(Menu.NONE, MENU_PREFILL, Menu.NONE, R.string.prefill_title); } } return super.onCreateOptionsMenu(menu); } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case android.R.id.home: onBackPressed(); return true; case R.id.edit_lang: displayLanguagesDialog(); return true; case R.id.clear: clearSurvey(); return true; case MENU_PREFILL: displayPrefillDialog(); return true; } return super.onOptionsItemSelected(item); } private void clearSurvey() { ViewUtil.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)); loadState(); spaceLeftOnCard(); } }); } private void displayLanguagesDialog() { // TODO: language management should be simplified LangsPreferenceData langsPrefData = LangsPreferenceUtil.createLangPrefData(this, mDatabase.getPreference(ConstantUtil.SURVEY_LANG_SETTING_KEY), mDatabase.getPreference(ConstantUtil.SURVEY_LANG_PRESENT_KEY)); final String[] langsSelectedNameArray = langsPrefData.getLangsSelectedNameArray(); final boolean[] langsSelectedBooleanArray = langsPrefData.getLangsSelectedBooleanArray(); final int[] langsSelectedMasterIndexArray = langsPrefData.getLangsSelectedMasterIndexArray(); ViewUtil.displayLanguageSelector(this, langsSelectedNameArray, langsSelectedBooleanArray, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int clicked) { if (dialog != null) { dialog.dismiss(); } mDatabase.savePreference(ConstantUtil.SURVEY_LANG_SETTING_KEY, LangsPreferenceUtil.formLangPreferenceString(langsSelectedBooleanArray, langsSelectedMasterIndexArray)); loadLanguages(); mAdapter.notifyOptionsChanged(); } }); } @Override public void onActivityResult(int requestCode, int resultCode, Intent data) { if (mRequestQuestionId == null) { return;// Move along, nothing to see here } if (requestCode == PHOTO_ACTIVITY_REQUEST || requestCode == VIDEO_ACTIVITY_REQUEST) { if (resultCode == RESULT_OK) { String fileSuffix; if (requestCode == PHOTO_ACTIVITY_REQUEST) { fileSuffix = IMAGE_SUFFIX; } else { fileSuffix = VIDEO_SUFFIX; } File tmp = getTmpFile(requestCode == PHOTO_ACTIVITY_REQUEST); // Ensure no image is saved in the DCIM folder FileUtil.cleanDCIM(this, tmp.getAbsolutePath()); String filename = PlatformUtil.uuid() + fileSuffix; File imgFile = new File(FileUtil.getFilesDir(FileType.MEDIA), filename); int maxImgSize = ConstantUtil.IMAGE_SIZE_320_240; String maxImgSizePref = mDatabase.getPreference(ConstantUtil.MAX_IMG_SIZE); if (!TextUtils.isEmpty(maxImgSizePref)) { maxImgSize = Integer.valueOf(maxImgSizePref); } String sizeTxt = getResources().getStringArray(R.array.max_image_size_pref)[maxImgSize]; if (ImageUtil.resizeImage(tmp.getAbsolutePath(), imgFile.getAbsolutePath(), maxImgSize)) { Toast.makeText(this, "Image resized to " + sizeTxt, Toast.LENGTH_LONG).show(); if (!tmp.delete()) { // must check return value to know if it failed Log.e(TAG, "Media file delete failed"); } } else if (!tmp.renameTo(imgFile)) { // must check return value to know if it failed! Log.e(TAG, "Media file resize failed"); } Bundle photoData = new Bundle(); photoData.putString(ConstantUtil.MEDIA_FILE_KEY, imgFile.getAbsolutePath()); mAdapter.onQuestionComplete(mRequestQuestionId, photoData); } else { Log.e(TAG, "Result of camera op was not ok: " + resultCode); } } else if (requestCode == SCAN_ACTIVITY_REQUEST && resultCode == RESULT_OK) { mAdapter.onQuestionComplete(mRequestQuestionId, data.getExtras()); } mRequestQuestionId = null;// Reset the tmp reference } private String getDefaultLang() { String lang = mSurvey.getLanguage(); if (TextUtils.isEmpty(lang)) { lang = ConstantUtil.ENGLISH_CODE; } return lang; } private void loadLanguages() { String langsSelection = mDatabase.getPreference(ConstantUtil.SURVEY_LANG_SETTING_KEY); String langsPresentIndexes = mDatabase.getPreference(ConstantUtil.SURVEY_LANG_PRESENT_KEY); LangsPreferenceData langsPrefData = LangsPreferenceUtil.createLangPrefData(this, langsSelection, langsPresentIndexes); mLanguages = LangsPreferenceUtil.getSelectedLangCodes(this, langsPrefData.getLangsSelectedMasterIndexArray(), langsPrefData.getLangsSelectedBooleanArray(), R.array.alllanguagecodes); } @Override public List<QuestionGroup> getQuestionGroups() { return mSurvey.getQuestionGroups(); } @Override public Map<String, QuestionResponse> getResponses() { return mQuestionResponses; } @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, SurveyDbAdapter.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); ViewUtil.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); } /** * 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())) { // fire off the intent Intent i = new Intent(android.provider.MediaStore.ACTION_IMAGE_CAPTURE); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(getTmpFile(true))); if (event.getSource() != null) { mRequestQuestionId = event.getSource().getQuestion().getId(); } else { Log.e(TAG, "Question source was null in the event"); } startActivityForResult(i, PHOTO_ACTIVITY_REQUEST); } else if (QuestionInteractionEvent.TAKE_VIDEO_EVENT.equals(event.getEventType())) { // fire off the intent Intent i = new Intent(android.provider.MediaStore.ACTION_VIDEO_CAPTURE); i.putExtra(android.provider.MediaStore.EXTRA_OUTPUT, Uri.fromFile(getTmpFile(false))); if (event.getSource() != null) { mRequestQuestionId = event.getSource().getQuestion().getId(); } else { Log.e(TAG, "Question source was null in the event"); } startActivityForResult(i, VIDEO_ACTIVITY_REQUEST); } else if (QuestionInteractionEvent.SCAN_BARCODE_EVENT.equals(event.getEventType())) { Intent intent = new Intent(ConstantUtil.BARCODE_SCAN_INTENT); try { startActivityForResult(intent, SCAN_ACTIVITY_REQUEST); if (event.getSource() != null) { mRequestQuestionId = event.getSource().getQuestion().getId(); } else { Log.e(TAG, "Question source was null in the event"); } } catch (ActivityNotFoundException ex) { AlertDialog.Builder builder = new AlertDialog.Builder(this); builder.setMessage(R.string.barcodeerror); builder.setPositiveButton(R.string.okbutton, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); builder.show(); } } else if (QuestionInteractionEvent.QUESTION_CLEAR_EVENT.equals(event.getEventType())) { String questionId = event.getSource().getQuestion().getId(); mQuestionResponses.remove(questionId); mDatabase.deleteResponse(mSurveyInstanceId, questionId); } else if (QuestionInteractionEvent.QUESTION_ANSWER_EVENT.equals(event.getEventType())) { 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 { mQuestionResponses.remove(questionId); mDatabase.deleteResponse(mSurveyInstanceId, questionId); } } } /* * Check SD card space. Warn by dialog popup if it is getting low. Return to * home screen if completely full. */ public void spaceLeftOnCard() { if (!Environment.getExternalStorageState().equals(Environment.MEDIA_MOUNTED)) { // TODO: more specific warning if card not mounted? } // compute space left StatFs stat = new StatFs(Environment.getExternalStorageDirectory().getPath()); double sdAvailSize = (double) stat.getAvailableBlocks() * (double) stat.getBlockSize(); // One binary gigabyte equals 1,073,741,824 bytes. // double gigaAvailable = sdAvailSize / 1073741824; // One binary megabyte equals 1 048 576 bytes. long megaAvailable = (long) Math.floor(sdAvailSize / 1048576.0); // keep track of changes SharedPreferences settings = getPreferences(MODE_PRIVATE); // assume we had space before long lastMegaAvailable = settings.getLong("cardMBAvaliable", 101L); SharedPreferences.Editor editor = settings.edit(); editor.putLong("cardMBAvaliable", megaAvailable); // Commit the edits! editor.commit(); if (megaAvailable <= 0L) {// All out, OR media not mounted // Bounce user ViewUtil.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 String s = getResources().getString(R.string.lowcardspacedialog); s = s.replace("%%%", Long.toString(megaAvailable)); ViewUtil.showConfirmDialog(R.string.lowcardspacetitle, s, 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 } } } } private File getTmpFile(boolean image) { String filename = image ? TEMP_PHOTO_NAME_PREFIX + IMAGE_SUFFIX : TEMP_VIDEO_NAME_PREFIX + VIDEO_SUFFIX; return new File(FileUtil.getFilesDir(FileType.TMP), filename); } }