/* * 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 com.radicaldynamic.groupinform.activities; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.InputStreamReader; import java.util.ArrayList; import java.util.Collections; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; import org.json.JSONTokener; import android.app.AlertDialog; import android.app.Dialog; import android.app.ListActivity; import android.app.ProgressDialog; import android.content.DialogInterface; import android.content.Intent; import android.os.AsyncTask; import android.os.Bundle; import android.os.Message; import android.util.Log; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.Window; import android.view.ContextMenu.ContextMenuInfo; import android.widget.ListView; import android.widget.RelativeLayout; import android.widget.TextView; import android.widget.Toast; import android.widget.AdapterView.AdapterContextMenuInfo; import com.radicaldynamic.groupinform.R; import com.radicaldynamic.groupinform.adapters.AccountFolderListAdapter; import com.radicaldynamic.groupinform.application.Collect; import com.radicaldynamic.groupinform.listeners.SynchronizeFoldersListener; import com.radicaldynamic.groupinform.logic.AccountDevice; import com.radicaldynamic.groupinform.logic.AccountFolder; import com.radicaldynamic.groupinform.logic.InformOnlineState; import com.radicaldynamic.groupinform.tasks.SynchronizeFoldersTask; import com.radicaldynamic.groupinform.utilities.HttpUtils; import com.radicaldynamic.groupinform.utilities.FileUtilsExtended; /* * Interface used to switch between folders and select destination * folders to copy and/or move forms and/or data to. */ public class AccountFolderList extends ListActivity implements SynchronizeFoldersListener { private static final String t = "AccountFolderList: "; private static final int MENU_ADD = Menu.FIRST; private static final int MENU_SYNC_LIST = Menu.FIRST + 1; private static final int CONTEXT_MENU_EDIT = Menu.FIRST; private static final int CONTEXT_MENU_INFO = Menu.FIRST + 1; public static final int DIALOG_DENIED_NOT_OWNER = 1; public static final int DIALOG_MORE_INFO = 2; // Intent keys public static final String KEY_COPY_TO_FOLDER = "key_copytofolder"; public static final String KEY_FOLDER_ID = "key_folderid"; public static final String KEY_FOLDER_NAME = "key_foldername"; private RefreshViewTask mRefreshViewTask; private SynchronizeFoldersTask mSynchronizeFoldersTask; private AccountFolder mFolder; private String mSelectedFolderId; private String mSelectedFolderName; private boolean mCopyToFolder = false; private ProgressDialog mProgressDialog; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); requestWindowFeature(Window.FEATURE_INDETERMINATE_PROGRESS); setContentView(R.layout.generic_list); if (savedInstanceState == null) { Intent intent = getIntent(); if (intent == null) { // Load defaults } else { mCopyToFolder = intent.getBooleanExtra(KEY_COPY_TO_FOLDER, false); } } else { if (savedInstanceState.containsKey(KEY_COPY_TO_FOLDER)) mCopyToFolder = savedInstanceState.getBoolean(KEY_COPY_TO_FOLDER); if (savedInstanceState.containsKey(KEY_FOLDER_ID)) mSelectedFolderId = savedInstanceState.getString(KEY_FOLDER_ID); if (savedInstanceState.containsKey(KEY_FOLDER_NAME)) mSelectedFolderName = savedInstanceState.getString(KEY_FOLDER_NAME); Object data = getLastNonConfigurationInstance(); if (data instanceof RefreshViewTask) mRefreshViewTask = (RefreshViewTask) data; else if (data instanceof SynchronizeFoldersTask) mSynchronizeFoldersTask = (SynchronizeFoldersTask) data; } if (mCopyToFolder) setTitle(getString(R.string.app_name) + " > " + getString(R.string.tf_copy_to_folder)); else setTitle(getString(R.string.app_name) + " > " + getString(R.string.tf_form_folders)); } @Override protected void onDestroy() { if (mProgressDialog != null && mProgressDialog.isShowing()) mProgressDialog.dismiss(); // Clean up folder synchronization task if (mSynchronizeFoldersTask != null) { mSynchronizeFoldersTask.setListener(null); if (mSynchronizeFoldersTask.getStatus() == AsyncTask.Status.FINISHED) { mSynchronizeFoldersTask.cancel(true); } } super.onDestroy(); } @Override protected void onResume() { super.onResume(); // Handle resume of folder synchronization task if (mSynchronizeFoldersTask != null) { mSynchronizeFoldersTask.setListener(this); if (mSynchronizeFoldersTask != null) { if (mSynchronizeFoldersTask.getStatus() == AsyncTask.Status.RUNNING) { synchronizationHandler(null); } else if (mSynchronizeFoldersTask.getStatus() == AsyncTask.Status.FINISHED) { if (mProgressDialog != null && mProgressDialog.isShowing()) { mProgressDialog.dismiss(); } } } } loadScreen(); } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); boolean enabled = false; boolean visible = true; if (Collect.getInstance().getIoService().isSignedIn()) enabled = true; if (mCopyToFolder) visible = false; if (!Collect.getInstance().getInformOnlineState().getDeviceRole().equals(AccountDevice.ROLE_DATA_ENTRY)) { menu.add(0, CONTEXT_MENU_EDIT, 0, getString(R.string.tf_edit_folder)).setEnabled(enabled).setVisible(visible); } menu.add(0, CONTEXT_MENU_INFO, 0, getString(R.string.tf_more_info)); } /* * (non-Javadoc) * * @see android.app.Activity#onCreateDialog(int) */ @Override protected Dialog onCreateDialog(int id) { AlertDialog.Builder builder = new AlertDialog.Builder(this); Dialog dialog = null; switch (id) { case DIALOG_DENIED_NOT_OWNER: builder .setIcon(R.drawable.ic_dialog_info) .setTitle(R.string.tf_unable_to_edit_folder_not_owner_title) .setMessage(R.string.tf_unable_to_edit_folder_not_owner_msg) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { dialog.cancel(); } }); dialog = builder.create(); break; case DIALOG_MORE_INFO: String message = "Owner:\n" + mFolder.getOwnerAlias() + "\n\n"; message = message + "Description:\n" + mFolder.getDescription(); builder .setIcon(R.drawable.ic_dialog_info) .setTitle(mFolder.getName()) .setMessage(message) .setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int id) { removeDialog(DIALOG_MORE_INFO); } }); dialog = builder.create(); break; } return dialog; } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); boolean enabled = false; boolean visible = true; if (Collect.getInstance().getIoService().isSignedIn()) enabled = true; if (mCopyToFolder) visible = false; if (!Collect.getInstance().getInformOnlineState().getDeviceRole().equals(AccountDevice.ROLE_DATA_ENTRY)) { menu.add(0, MENU_ADD, 0, getString(R.string.tf_create_folder)) .setIcon(R.drawable.ic_menu_add) .setEnabled(enabled) .setVisible(visible); } menu.add(0, MENU_SYNC_LIST, 0, getString(R.string.tf_replication_list)) .setIcon(R.drawable.ic_menu_sync_list) .setEnabled(enabled) .setVisible(visible); return true; } @Override public boolean onContextItemSelected(MenuItem item) { AdapterContextMenuInfo info = (AdapterContextMenuInfo) item.getMenuInfo(); AccountFolder folder = (AccountFolder) getListView().getItemAtPosition(info.position); switch (item.getItemId()) { case CONTEXT_MENU_EDIT: if (Collect.getInstance().getInformOnlineState().getDeviceId().equals(folder.getOwnerId())) { Intent i = new Intent(this, AccountFolderActivity.class); i.putExtra(AccountFolderActivity.KEY_FOLDER_ID, folder.getId()); i.putExtra(AccountFolderActivity.KEY_FOLDER_REV, folder.getRev()); i.putExtra(AccountFolderActivity.KEY_FOLDER_OWNER, folder.getOwnerId()); i.putExtra(AccountFolderActivity.KEY_FOLDER_NAME, folder.getName()); i.putExtra(AccountFolderActivity.KEY_FOLDER_DESC, folder.getDescription()); i.putExtra(AccountFolderActivity.KEY_FOLDER_VISIBILITY, folder.getVisibility()); startActivity(i); } else { showDialog(DIALOG_DENIED_NOT_OWNER); } break; case CONTEXT_MENU_INFO: mFolder = folder; showDialog(DIALOG_MORE_INFO); break; default: return super.onContextItemSelected(item); } return true; } /** * Stores the path of selected form and finishes. */ @Override protected void onListItemClick(ListView listView, View view, int position, long id) { mFolder = (AccountFolder) getListAdapter().getItem(position); // Store separately because mFolder may not be available after activity restart mSelectedFolderId = mFolder.getId(); mSelectedFolderName = mFolder.getName(); if (mFolder.isReplicated() && !Collect.getInstance().getDbService().isDbLocal(mFolder.getId())) { mSynchronizeFoldersTask = new SynchronizeFoldersTask(); mSynchronizeFoldersTask.setListener(AccountFolderList.this); mSynchronizeFoldersTask.setTransferMode(SynchronizeFoldersListener.MODE_PULL); mSynchronizeFoldersTask.setFolders(new ArrayList<String>(Collections.singletonList(mFolder.getId()))); mSynchronizeFoldersTask.execute(); } else { openFolder(); } } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case MENU_ADD: startActivity(new Intent(this, AccountFolderActivity.class).putExtra(AccountFolderActivity.KEY_NEW_FOLDER, true)); break; case MENU_SYNC_LIST: startActivity(new Intent(this, AccountFolderReplicationList.class)); break; } return super.onOptionsItemSelected(item); } @Override public Object onRetainNonConfigurationInstance() { if (mRefreshViewTask != null && mRefreshViewTask.getStatus() != AsyncTask.Status.FINISHED) return mRefreshViewTask; if (mSynchronizeFoldersTask != null && mSynchronizeFoldersTask.getStatus() != AsyncTask.Status.FINISHED) return mSynchronizeFoldersTask; return null; } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean(KEY_COPY_TO_FOLDER, mCopyToFolder); outState.putString(KEY_FOLDER_ID, mSelectedFolderId); outState.putString(KEY_FOLDER_NAME, mSelectedFolderName); } /* * Refresh the main form browser view as requested by the user */ private class RefreshViewTask extends AsyncTask<Void, Void, Void> { private ArrayList<AccountFolder> folders = new ArrayList<AccountFolder>(); @Override protected Void doInBackground(Void... nothing) { if (FileUtilsExtended.isFileOlderThan(getCacheDir() + File.separator + FileUtilsExtended.FOLDER_CACHE_FILE, FileUtilsExtended.TIME_TWO_MINUTES)) fetchFolderList(false); folders = loadFolderList(); return null; } @Override protected void onPreExecute() { setProgressBarIndeterminateVisibility(true); } @Override protected void onPostExecute(Void nothing) { RelativeLayout onscreenProgress = (RelativeLayout) findViewById(R.id.progress); onscreenProgress.setVisibility(View.GONE); if (folders.isEmpty()) { TextView nothingToDisplay = (TextView) findViewById(R.id.nothingToDisplay); nothingToDisplay.setVisibility(View.VISIBLE); } else { AccountFolderListAdapter adapter; adapter = new AccountFolderListAdapter( getApplicationContext(), R.layout.folder_list_item, folders); setListAdapter(adapter); } setProgressBarIndeterminateVisibility(false); } } /* * Fetch a new folder list from Inform Online and store it on disk */ public static void fetchFolderList(boolean fetchAnyway) { if (Collect.getInstance().getIoService().isSignedIn() || fetchAnyway) { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "fetching list of folders"); } else { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG, t + "not signed in, skipping folder list fetch"); return; } // Try to ping the service to see if it is "up" String folderListUrl = Collect.getInstance().getInformOnlineState().getServerUrl() + "/folder/list"; String getResult = HttpUtils.getUrlData(folderListUrl); JSONObject jsonFolderList; try { jsonFolderList = (JSONObject) new JSONTokener(getResult).nextValue(); String result = jsonFolderList.optString(InformOnlineState.RESULT, InformOnlineState.ERROR); if (result.equals(InformOnlineState.OK)) { // Write out list of jsonFolders for later retrieval by loadFoldersList() JSONArray jsonFolders = jsonFolderList.getJSONArray("folders"); try { // Write out a folder list cache file FileOutputStream fos = new FileOutputStream(new File(Collect.getInstance().getCacheDir(), FileUtilsExtended.FOLDER_CACHE_FILE)); fos.write(jsonFolders.toString().getBytes()); fos.close(); } catch (Exception e) { if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "unable to write folder cache: " + e.toString()); e.printStackTrace(); } } else { // There was a problem.. handle it! } } catch (NullPointerException e) { // Communication error if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "no getResult to parse. Communication error with node.js server?"); e.printStackTrace(); } catch (JSONException e) { // Parse error (malformed result) if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "failed to parse getResult " + getResult); e.printStackTrace(); } } public static ArrayList<AccountFolder> loadFolderList() { if (Collect.Log.DEBUG) Log.d(Collect.LOGTAG , t + "loading folder cache"); ArrayList<AccountFolder> folders = new ArrayList<AccountFolder>(); if (!new File(Collect.getInstance().getCacheDir(), FileUtilsExtended.FOLDER_CACHE_FILE).exists()) { if (Collect.Log.WARN) Log.w(Collect.LOGTAG, t + "folder cache file cannot be read: aborting loadFolderList()"); return folders; } try { FileInputStream fis = new FileInputStream(new File(Collect.getInstance().getCacheDir(), FileUtilsExtended.FOLDER_CACHE_FILE)); InputStreamReader reader = new InputStreamReader(fis); BufferedReader buffer = new BufferedReader(reader, 8192); StringBuilder sb = new StringBuilder(); String cur; while ((cur = buffer.readLine()) != null) { sb.append(cur + "\n"); } buffer.close(); reader.close(); fis.close(); try { JSONArray jsonFolders = (JSONArray) new JSONTokener(sb.toString()).nextValue(); for (int i = 0; i < jsonFolders.length(); i++) { JSONObject jsonFolder = jsonFolders.getJSONObject(i); AccountFolder folder = new AccountFolder( jsonFolder.getString("id"), jsonFolder.getString("rev"), jsonFolder.getString("owner"), jsonFolder.getString("name"), jsonFolder.getString("description"), jsonFolder.getString("visibility"), jsonFolder.getBoolean("replication")); folders.add(folder); // Also update the account folder hash since this will be needed by BrowserActivity, among other things Collect.getInstance().getInformOnlineState().getAccountFolders().put(folder.getId(), folder); } } catch (JSONException e) { // Parse error (malformed result) if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "failed to parse JSON " + sb.toString()); e.printStackTrace(); } } catch (Exception e) { if (Collect.Log.ERROR) Log.e(Collect.LOGTAG, t + "unable to read folder cache: " + e.toString()); e.printStackTrace(); } return folders; } @Override public void synchronizationHandler(Message msg) { if (msg == null) { // Close any existing progress dialogs if (mProgressDialog != null && mProgressDialog.isShowing()) mProgressDialog.dismiss(); // Start new dialog with suitable message mProgressDialog = new ProgressDialog(AccountFolderList.this); mProgressDialog.setMessage(getString(R.string.tf_synchronizing_folders_dialog_msg)); mProgressDialog.show(); } else { // Update progress dialog if (mProgressDialog != null && mProgressDialog.isShowing()) { mProgressDialog.setMessage(getString(R.string.tf_synchronizing_folder_count_dialog_msg, msg.arg1, msg.arg2)); } } } @Override public void synchronizationTaskFinished(Bundle data) { if (mProgressDialog != null && mProgressDialog.isShowing()) mProgressDialog.dismiss(); if (data.getBoolean(SynchronizeFoldersListener.SUCCESSFUL)) { openFolder(); } else { Toast.makeText(getApplicationContext(), "Unable to open " + mSelectedFolderName + ". Please try again in a few minutes.", Toast.LENGTH_LONG).show(); } } /** * Load the various elements of the screen that must wait for other tasks to * complete */ private void loadScreen() { mRefreshViewTask = new RefreshViewTask(); mRefreshViewTask.execute(); registerForContextMenu(getListView()); if (mCopyToFolder) { Toast.makeText(getApplicationContext(), "Select destination folder", Toast.LENGTH_SHORT).show(); } } private void openFolder() { setProgressBarIndeterminateVisibility(true); if (mCopyToFolder) { Intent i = new Intent(); i.putExtra(KEY_FOLDER_NAME, mFolder.getName()); i.putExtra(KEY_FOLDER_ID, mFolder.getId()); setResult(RESULT_OK, i); finish(); } else { Collect.getInstance().getInformOnlineState().setSelectedDatabase(mSelectedFolderId); finish(); } } }