/* * Copyright (C) 2009 University of Washington * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except * in compliance with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software distributed under the License * is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing permissions and limitations under * the License. */ package org.odk.collect.android.activities; import org.odk.collect.android.R; import org.odk.collect.android.listeners.FormDownloaderListener; import org.odk.collect.android.listeners.FormListDownloaderListener; import org.odk.collect.android.logic.FormDetails; import org.odk.collect.android.preferences.PreferencesActivity; import org.odk.collect.android.tasks.DownloadFormListTask; import org.odk.collect.android.tasks.DownloadFormsTask; import org.odk.collect.android.utilities.WebUtils; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.content.SharedPreferences; import android.graphics.Color; import android.net.ConnectivityManager; import android.net.NetworkInfo; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.preference.PreferenceManager; import android.util.Log; import android.util.SparseBooleanArray; import android.view.LayoutInflater; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.View.OnClickListener; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import android.widget.SimpleAdapter; import android.widget.Toast; import java.util.ArrayList; import java.util.HashMap; import java.util.Set; /** * Responsible for displaying, adding and deleting all the valid forms in the forms directory. One * caveat. If the server requires authentication, a dialog will pop up asking when you request the * form list. If somehow you manage to wait long enough and then try to download selected forms and * your authorization has timed out, it won't again ask for authentication, it will just throw a 401 * and you'll have to hit 'refresh' where it will ask for credentials again. Technically a server * could point at other servers requiring authentication to download the forms, but the current * implementation in Collect doesn't allow for that. Mostly this is just because it's a pain in the * butt to keep track of which forms we've downloaded and where we're needing to authenticate. I * think we do something similar in the instanceuploader task/activity, so should change the * implementation eventually. * * @author Carl Hartung (carlhartung@gmail.com) */ public class FormDownloadList extends ListActivity implements FormListDownloaderListener, FormDownloaderListener { private static final String t = "RemoveFileManageList"; private static final int PROGRESS_DIALOG = 1; private static final int AUTH_DIALOG = 2; private static final int MENU_PREFERENCES = Menu.FIRST; private static final String BUNDLE_TOGGLED_KEY = "toggled"; private static final String BUNDLE_SELECTED_COUNT = "selectedcount"; private static final String BUNDLE_FORM_MAP = "formmap"; private static final String DIALOG_TITLE = "dialogtitle"; private static final String DIALOG_MSG = "dialogmsg"; private static final String DIALOG_SHOWING = "dialogshowing"; private static final String FORMLIST = "formlist"; public static final String LIST_URL = "listurl"; private static final String FORMNAME = "formname"; private static final String FORMID = "formid"; private static final String FORMID_DISPLAY = "formiddisplay"; private String mAlertMsg; private boolean mAlertShowing = false; private String mAlertTitle; private AlertDialog mAlertDialog; private ProgressDialog mProgressDialog; private Button mDownloadButton; private DownloadFormListTask mDownloadFormListTask; private DownloadFormsTask mDownloadFormsTask; private Button mToggleButton; private Button mRefreshButton; private HashMap<String, FormDetails> mFormNamesAndURLs; private SimpleAdapter mFormListAdapter; private ArrayList<HashMap<String, String>> mFormList; private boolean mToggled = false; private int mSelectedCount = 0; private static final boolean EXIT = true; private static final boolean DO_NOT_EXIT = false; private boolean mShouldExit; private static final String SHOULD_EXIT = "shouldexit"; /* * (non-Javadoc) * @see android.app.Activity#onCreate(android.os.Bundle) */ @SuppressWarnings("unchecked") @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.remote_file_manage_list); setTitle(getString(R.string.app_name) + " > " + getString(R.string.get_forms)); mAlertMsg = getString(R.string.please_wait); // need white background before load getListView().setBackgroundColor(Color.WHITE); mDownloadButton = (Button) findViewById(R.id.add_button); mDownloadButton.setEnabled(selectedItemCount() > 0); mDownloadButton.setOnClickListener(new OnClickListener() { /* * (non-Javadoc) * @see android.view.View.OnClickListener#onClick(android.view.View) */ @Override public void onClick(View v) { downloadSelectedFiles(); mToggled = false; clearChoices(); } }); mToggleButton = (Button) findViewById(R.id.toggle_button); mToggleButton.setOnClickListener(new OnClickListener() { /* * (non-Javadoc) * @see android.view.View.OnClickListener#onClick(android.view.View) */ @Override public void onClick(View v) { // toggle selections of items to all or none ListView ls = getListView(); mToggled = !mToggled; for (int pos = 0; pos < ls.getCount(); pos++) { ls.setItemChecked(pos, mToggled); } mDownloadButton.setEnabled(!(selectedItemCount() == 0)); } }); mRefreshButton = (Button) findViewById(R.id.refresh_button); mRefreshButton.setOnClickListener(new OnClickListener() { /* * (non-Javadoc) * @see android.view.View.OnClickListener#onClick(android.view.View) */ @Override public void onClick(View v) { mToggled = false; downloadFormList(); FormDownloadList.this.getListView().clearChoices(); clearChoices(); } }); if (savedInstanceState != null) { // If the screen has rotated, the hashmap with the form ids and urls is passed here. if (savedInstanceState.containsKey(BUNDLE_FORM_MAP)) { mFormNamesAndURLs = (HashMap<String, FormDetails>) savedInstanceState .getSerializable(BUNDLE_FORM_MAP); } // indicating whether or not select-all is on or off. if (savedInstanceState.containsKey(BUNDLE_TOGGLED_KEY)) { mToggled = savedInstanceState.getBoolean(BUNDLE_TOGGLED_KEY); } // how many items we've selected // Android should keep track of this, but broken on rotate... if (savedInstanceState.containsKey(BUNDLE_SELECTED_COUNT)) { mSelectedCount = savedInstanceState.getInt(BUNDLE_SELECTED_COUNT); mDownloadButton.setEnabled(!(mSelectedCount == 0)); } // to restore alert dialog. if (savedInstanceState.containsKey(DIALOG_TITLE)) { mAlertTitle = savedInstanceState.getString(DIALOG_TITLE); } if (savedInstanceState.containsKey(DIALOG_MSG)) { mAlertMsg = savedInstanceState.getString(DIALOG_MSG); } if (savedInstanceState.containsKey(DIALOG_SHOWING)) { mAlertShowing = savedInstanceState.getBoolean(DIALOG_SHOWING); } if (savedInstanceState.containsKey(SHOULD_EXIT)) { mShouldExit = savedInstanceState.getBoolean(SHOULD_EXIT); } } if (savedInstanceState != null && savedInstanceState.containsKey(FORMLIST)) { mFormList = (ArrayList<HashMap<String, String>>) savedInstanceState.getSerializable(FORMLIST); } else { mFormList = new ArrayList<HashMap<String, String>>(); } if (getLastNonConfigurationInstance() instanceof DownloadFormListTask) { mDownloadFormListTask = (DownloadFormListTask) getLastNonConfigurationInstance(); if (mDownloadFormListTask.getStatus() == AsyncTask.Status.FINISHED) { try { dismissDialog(PROGRESS_DIALOG); } catch (IllegalArgumentException e) { Log.i(t, "Attempting to close a dialog that was not previously opened"); } } } else if (getLastNonConfigurationInstance() instanceof DownloadFormsTask) { mDownloadFormsTask = (DownloadFormsTask) getLastNonConfigurationInstance(); if (mDownloadFormsTask.getStatus() == AsyncTask.Status.FINISHED) { try { dismissDialog(PROGRESS_DIALOG); } catch (IllegalArgumentException e) { Log.i(t, "Attempting to close a dialog that was not previously opened"); } } } else if (getLastNonConfigurationInstance() == null) { // first time, so get the formlist downloadFormList(); } String[] data = new String[] { FORMNAME, FORMID_DISPLAY, FORMID }; int[] view = new int[] { R.id.text1, R.id.text2 }; mFormListAdapter = new SimpleAdapter(this, mFormList, R.layout.two_item_multiple_choice, data, view); getListView().setChoiceMode(ListView.CHOICE_MODE_MULTIPLE); getListView().setItemsCanFocus(false); setListAdapter(mFormListAdapter); } private void clearChoices() { FormDownloadList.this.getListView().clearChoices(); mDownloadButton.setEnabled(false); } /* * (non-Javadoc) * @see android.app.ListActivity#onListItemClick(android.widget.ListView, android.view.View, int, long) */ @Override protected void onListItemClick(ListView l, View v, int position, long id) { super.onListItemClick(l, v, position, id); mDownloadButton.setEnabled(!(selectedItemCount() == 0)); } /** * Starts the download task and shows the progress dialog. */ private void downloadFormList() { ConnectivityManager connectivityManager = (ConnectivityManager) getSystemService(Context.CONNECTIVITY_SERVICE); NetworkInfo ni = connectivityManager.getActiveNetworkInfo(); if (ni == null || !ni.isConnected()) { Toast.makeText(this, R.string.no_connection, Toast.LENGTH_SHORT).show(); } else { mFormNamesAndURLs = new HashMap<String, FormDetails>(); if (mProgressDialog != null) { // This is needed because onPrepareDialog() is broken in 1.6. mProgressDialog.setMessage(getString(R.string.please_wait)); } showDialog(PROGRESS_DIALOG); mDownloadFormListTask = new DownloadFormListTask(); mDownloadFormListTask.setDownloaderListener(this); mDownloadFormListTask.execute(); } } /* * (non-Javadoc) * @see android.app.Activity#onSaveInstanceState(android.os.Bundle) */ @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(BUNDLE_TOGGLED_KEY, mToggled); outState.putInt(BUNDLE_SELECTED_COUNT, selectedItemCount()); outState.putSerializable(BUNDLE_FORM_MAP, mFormNamesAndURLs); outState.putString(DIALOG_TITLE, mAlertTitle); outState.putString(DIALOG_MSG, mAlertMsg); outState.putBoolean(DIALOG_SHOWING, mAlertShowing); outState.putBoolean(SHOULD_EXIT, mShouldExit); outState.putSerializable(FORMLIST, mFormList); } /** * returns the number of items currently selected in the list. * * @return */ private int selectedItemCount() { int count = 0; SparseBooleanArray sba = getListView().getCheckedItemPositions(); for (int i = 0; i < getListView().getCount(); i++) { if (sba.get(i, false)) { count++; } } return count; } /* * (non-Javadoc) * @see android.app.Activity#onCreateOptionsMenu(android.view.Menu) */ @Override public boolean onCreateOptionsMenu(Menu menu) { menu.add(0, MENU_PREFERENCES, 0, getString(R.string.general_preferences)).setIcon( android.R.drawable.ic_menu_preferences); return true; } /* * (non-Javadoc) * @see android.app.Activity#onMenuItemSelected(int, android.view.MenuItem) */ @Override public boolean onMenuItemSelected(int featureId, MenuItem item) { switch (item.getItemId()) { case MENU_PREFERENCES: Intent i = new Intent(this, PreferencesActivity.class); startActivity(i); return true; } return super.onMenuItemSelected(featureId, item); } /* * (non-Javadoc) * @see android.app.Activity#onCreateDialog(int) */ @Override protected Dialog onCreateDialog(int id) { switch (id) { case PROGRESS_DIALOG: mProgressDialog = new ProgressDialog(this); DialogInterface.OnClickListener loadingButtonListener = new DialogInterface.OnClickListener() { /* * (non-Javadoc) * @see android.content.DialogInterface.OnClickListener#onClick(android.content.DialogInterface, int) */ @Override public void onClick(DialogInterface dialog, int which) { dialog.dismiss(); // we use the same progress dialog for both // so whatever isn't null is running if (mDownloadFormListTask != null) { mDownloadFormListTask.setDownloaderListener(null); mDownloadFormListTask.cancel(true); } if (mDownloadFormsTask != null) { mDownloadFormsTask.cancel(true); mDownloadFormsTask.setDownloaderListener(null); } } }; mProgressDialog.setTitle(getString(R.string.downloading_data)); mProgressDialog.setMessage(mAlertMsg); mProgressDialog.setIcon(android.R.drawable.ic_dialog_info); mProgressDialog.setIndeterminate(true); mProgressDialog.setCancelable(false); mProgressDialog.setButton(getString(R.string.cancel), loadingButtonListener); return mProgressDialog; case AUTH_DIALOG: AlertDialog.Builder b = new AlertDialog.Builder(this); LayoutInflater factory = LayoutInflater.from(this); final View dialogView = factory.inflate(R.layout.server_auth_dialog, null); // Get the server, username, and password from the settings SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); String server = settings.getString(PreferencesActivity.KEY_SERVER_URL, getString(R.string.default_server_url)); final String url = server + settings.getString(PreferencesActivity.KEY_FORMLIST_URL, "/formList"); Log.i(t, "Trying to get formList from: " + url); EditText username = (EditText) dialogView.findViewById(R.id.username_edit); String storedUsername = settings.getString(PreferencesActivity.KEY_USERNAME, null); username.setText(storedUsername); EditText password = (EditText) dialogView.findViewById(R.id.password_edit); String storedPassword = settings.getString(PreferencesActivity.KEY_PASSWORD, null); password.setText(storedPassword); b.setTitle(getString(R.string.server_requires_auth)); b.setMessage(getString(R.string.server_auth_credentials, url)); b.setView(dialogView); b.setPositiveButton(getString(R.string.ok), new DialogInterface.OnClickListener() { /* * (non-Javadoc) * @see android.content.DialogInterface.OnClickListener#onClick(android.content.DialogInterface, int) */ @Override public void onClick(DialogInterface dialog, int which) { EditText username = (EditText) dialogView.findViewById(R.id.username_edit); EditText password = (EditText) dialogView.findViewById(R.id.password_edit); Uri u = Uri.parse(url); WebUtils.addCredentials(username.getText().toString(), password.getText() .toString(), u.getHost()); downloadFormList(); } }); b.setNegativeButton(getString(R.string.cancel), new DialogInterface.OnClickListener() { /* * (non-Javadoc) * @see android.content.DialogInterface.OnClickListener#onClick(android.content.DialogInterface, int) */ @Override public void onClick(DialogInterface dialog, int which) { finish(); } }); b.setCancelable(false); mAlertShowing = false; return b.create(); } return null; } /** * starts the task to download the selected forms, also shows progress dialog */ @SuppressWarnings("unchecked") private void downloadSelectedFiles() { int totalCount = 0; ArrayList<FormDetails> filesToDownload = new ArrayList<FormDetails>(); SparseBooleanArray sba = getListView().getCheckedItemPositions(); for (int i = 0; i < getListView().getCount(); i++) { if (sba.get(i, false)) { HashMap<String, String> item = (HashMap<String, String>) getListAdapter().getItem(i); filesToDownload.add(mFormNamesAndURLs.get(item.get(FORMID))); } } totalCount = filesToDownload.size(); if (totalCount > 0) { // show dialog box showDialog(PROGRESS_DIALOG); mDownloadFormsTask = new DownloadFormsTask(); SharedPreferences settings = PreferenceManager.getDefaultSharedPreferences(getBaseContext()); String auth = settings.getString(PreferencesActivity.KEY_AUTH, ""); mDownloadFormsTask.setAuth(auth); mDownloadFormsTask.setDownloaderListener(this); mDownloadFormsTask.execute(filesToDownload); } else { Toast.makeText(getApplicationContext(), R.string.noselect_error, Toast.LENGTH_SHORT) .show(); } } /* * (non-Javadoc) * @see android.app.Activity#onRetainNonConfigurationInstance() */ @Override public Object onRetainNonConfigurationInstance() { if (mDownloadFormsTask != null) { return mDownloadFormsTask; } else { return mDownloadFormListTask; } } /* * (non-Javadoc) * @see android.app.ListActivity#onDestroy() */ @Override protected void onDestroy() { if (mDownloadFormListTask != null) { mDownloadFormListTask.setDownloaderListener(null); } if (mDownloadFormsTask != null) { mDownloadFormsTask.setDownloaderListener(null); } super.onDestroy(); } /* * (non-Javadoc) * @see android.app.Activity#onResume() */ @Override protected void onResume() { if (mDownloadFormListTask != null) { mDownloadFormListTask.setDownloaderListener(this); } if (mDownloadFormsTask != null) { mDownloadFormsTask.setDownloaderListener(this); } if (mAlertShowing) { createAlertDialog(mAlertTitle, mAlertMsg, mShouldExit); } super.onResume(); } /* * (non-Javadoc) * @see android.app.Activity#onPause() */ @Override protected void onPause() { if (mAlertDialog != null && mAlertDialog.isShowing()) { mAlertDialog.dismiss(); } super.onPause(); } /** * Called when the form list has finished downloading. results will either contain a set of * <formname, formdetails> tuples, or one tuple of DL.ERROR.MSG and the associated message. * * @param result */ public void formListDownloadingComplete(HashMap<String, FormDetails> result) { dismissDialog(PROGRESS_DIALOG); mDownloadFormListTask.setDownloaderListener(null); if (result == null) { Log.e(t, "Formlist Downloading returned null. That shouldn't happen"); // Just displayes "error occured" to the user, but this should never happen. createAlertDialog(getString(R.string.load_remote_form_error), getString(R.string.error_occured), EXIT); return; } if (result.containsKey(DownloadFormListTask.DL_AUTH_REQUIRED)) { // need authorization showDialog(AUTH_DIALOG); } else if (result.containsKey(DownloadFormListTask.DL_ERROR_MSG)) { // Download failed String dialogMessage = getString(R.string.list_failed_with_error, result.get(DownloadFormListTask.DL_ERROR_MSG).errorStr); String dialogTitle = getString(R.string.load_remote_form_error); createAlertDialog(dialogTitle, dialogMessage, DO_NOT_EXIT); } else { // Everything worked. Clear the list and add the results. mFormNamesAndURLs = result; mFormList.clear(); ArrayList<String> ids = new ArrayList<String>(mFormNamesAndURLs.keySet()); for (int i = 0; i < result.size(); i++) { HashMap<String, String> item = new HashMap<String, String>(); item.put(FORMNAME, mFormNamesAndURLs.get(ids.get(i)).formName); item.put(FORMID_DISPLAY, "ID: " + mFormNamesAndURLs.get(ids.get(i)).formID); item.put(FORMID, mFormNamesAndURLs.get(ids.get(i)).formID); // Insert the new form in alphabetical order. if (mFormList.size() == 0) { mFormList.add(item); } else { int j; for (j = 0; j < mFormList.size(); j++) { HashMap<String, String> compareMe = mFormList.get(j); String name = compareMe.get(FORMNAME); if (name.compareTo(mFormNamesAndURLs.get(ids.get(i)).formName) > 0) { break; } } mFormList.add(j, item); } } mFormListAdapter.notifyDataSetChanged(); } } /** * Creates an alert dialog with the given tite and message. If shouldExit is set to true, the * activity will exit when the user clicks "ok". * * @param title * @param message * @param shouldExit */ private void createAlertDialog(String title, String message, final boolean shouldExit) { mAlertDialog = new AlertDialog.Builder(this).create(); mAlertDialog.setTitle(title); mAlertDialog.setMessage(message); DialogInterface.OnClickListener quitListener = new DialogInterface.OnClickListener() { /* * (non-Javadoc) * @see android.content.DialogInterface.OnClickListener#onClick(android.content.DialogInterface, int) */ @Override public void onClick(DialogInterface dialog, int i) { switch (i) { case DialogInterface.BUTTON1: // ok // just close the dialog mAlertShowing = false; // successful download, so quit if (shouldExit) { finish(); } break; } } }; mAlertDialog.setCancelable(false); mAlertDialog.setButton(getString(R.string.ok), quitListener); mAlertDialog.setIcon(android.R.drawable.ic_dialog_info); mAlertMsg = message; mAlertTitle = title; mAlertShowing = true; mShouldExit = shouldExit; mAlertDialog.show(); } /* * (non-Javadoc) * @see org.odk.collect.android.listeners.FormDownloaderListener#progressUpdate(java.lang.String, int, int) */ @Override public void progressUpdate(String currentFile, int progress, int total) { mAlertMsg = getString(R.string.fetching_file, currentFile, progress, total); mProgressDialog.setMessage(mAlertMsg); } /* * (non-Javadoc) * @see org.odk.collect.android.listeners.FormDownloaderListener#formsDownloadingComplete(java.util.HashMap) */ @Override public void formsDownloadingComplete(HashMap<String, String> result) { if (mDownloadFormsTask != null) { mDownloadFormsTask.setDownloaderListener(null); } if (mProgressDialog.isShowing()) { // should always be true here mProgressDialog.dismiss(); } StringBuilder b = new StringBuilder(); Set<String> keys = result.keySet(); for (String k : keys) { b.append(k + " - " + result.get(k)); b.append("\n\n"); } createAlertDialog(getString(R.string.download_forms_result), b.toString().trim(), EXIT); } }