/*
* 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);
}
}