package pk.contender.earmouse;
import android.app.Activity;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.res.Resources;
import android.net.http.AndroidHttpClient;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.JsonReader;
import android.util.Log;
import android.view.ActionMode;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AbsListView;
import android.widget.Button;
import android.widget.ListView;
import android.widget.Toast;
import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.HttpResponse;
import org.apache.http.message.BasicHttpRequest;
import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Activity for displaying a list of Modules available for remote installation and installing these modules either one by one or in a batch.
* @author Paul Klinkenberg <pklinken.development@gmail.com>
*/
public class ModuleManagerActivity extends Activity implements ManagerListFragment.OnModuleSelectedListener {
/* SharedPreferences constants */
private static final String PREFERENCES_MODULEMANAGERACTIVITY_SELECTIONPOSITION = "preferences_ModuleManagerActivity_selectionPosition";
/** The index of the current selection in the ListView */
private int selectionPosition = -1;
/**
* when in ActionMode, this List contains all the Modules the user currently has
* selected.
*/
private List<Module> selection = null;
/**
* reference to the Activity's ActionMode, is null if the Activity is not in ActionMode
*/
private ActionMode mActionMode;
/**
* Adapter used by this Activity's ListView.
*/
public static ModuleListAdapter mAdapter;
/** List of currently shown Modules in the ListView, */
public static final List<Module> shownModuleList = new ArrayList<>();
/**
* Loads saved state and preferences and sets up the context actionbar (CAB).
* Fetches a list of available modules from the server.
* @param savedInstanceState the previously saved instance state, can be null.
*/
@Override
protected void onCreate(final Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_module_manager);
SharedPreferences settings = getSharedPreferences(Main.PREFS_NAME, Activity.MODE_PRIVATE);
selectionPosition = settings.getInt(PREFERENCES_MODULEMANAGERACTIVITY_SELECTIONPOSITION, -1);
ListView lv = null;
ManagerListFragment managerListFragment = (ManagerListFragment) getFragmentManager().findFragmentById(R.id.fragmentModuleList);
if(managerListFragment != null){
lv = managerListFragment.getListView();
mAdapter = (ModuleListAdapter) managerListFragment.getListAdapter();
managerListFragment.setEmptyText(getString(R.string.list_no_modules_available));
managerListFragment.setListShown(false);
new FetchListJsonFromServer().execute();
}
if(lv != null) {
lv.setChoiceMode(ListView.CHOICE_MODE_MULTIPLE_MODAL);
final ListView finalLv = lv;
lv.setMultiChoiceModeListener(new AbsListView.MultiChoiceModeListener() {
@Override
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
if (checked)
selection.add((Module) finalLv.getItemAtPosition(position));
else
//noinspection RedundantCast
selection.remove((Module)finalLv.getItemAtPosition(position));
if (selection.size() > 0)
mode.setTitle(selection.size() + " " + getString(R.string.cab_selected));
else
mode.setTitle("");
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
// Respond to clicks on the actions in the CAB
switch (item.getItemId()) {
case R.id.manager_ctx_install:
new fetchAndInstallSelection().execute();
mode.finish(); // Action picked, so close the CAB
return true;
default:
return false;
}
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
// Inflate the menu for the CAB
MenuInflater inflater = mode.getMenuInflater();
inflater.inflate(R.menu.manager_context, menu);
mActionMode = mode;
selection = new ArrayList<>();
// disable Install button, it's ambiguous and screws up the UI
Button installButton = (Button) findViewById(R.id.manager_button);
if(installButton != null) {
installButton.animate().setDuration(1000).alpha((float) 0.2);
installButton.setClickable(false);
}
if(savedInstanceState != null && savedInstanceState.getBoolean("isInActionMode")) {
// If there is a saved state and we were in ActionMode try to restore the previous
// selection. This happens when the screen is rotated in ActionMode.
for (int i = 0;i < finalLv.getCount(); i++) {
if(finalLv.isItemChecked(i)) {
Module mod = (Module) finalLv.getItemAtPosition(i);
if (mod != null)
selection.add(mod);
}
}
mode.setTitle(selection.size() + " " + getString(R.string.cab_selected));
}
return true;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
// Enable install button when we exit CAB
Button installButton = (Button) findViewById(R.id.manager_button);
if(installButton != null) {
installButton.setClickable(true);
installButton.animate().setDuration(1000).alpha((float) 1.0);
}
mActionMode = null;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
// Here you can perform updates to the CAB due to
// an invalidate() request
return false;
}
});
}
}
@Override
protected void onPause() {
super.onPause();
SharedPreferences settings = getSharedPreferences(Main.PREFS_NAME, Activity.MODE_PRIVATE);
settings.edit().putInt(PREFERENCES_MODULEMANAGERACTIVITY_SELECTIONPOSITION, selectionPosition).apply();
}
@Override
protected void onSaveInstanceState(@SuppressWarnings("NullableProblems") Bundle outState) {
// If we are in ActionMode save this state
if(outState != null)
outState.putBoolean("isInActionMode", (mActionMode != null));
// FIXME: Shouldn't this go in sharedprefs
//noinspection ConstantConditions
super.onSaveInstanceState(outState);
}
/**
* Shows the selected Module in detail, either by updating {@link pk.contender.earmouse.ManagerDetailsFragment} if available or otherwise
* starting an Intent for {@link pk.contender.earmouse.ManagerDetailActivity}
* @param position The position in the ListView of the user selection
*/
@Override
public void onModuleSelected(int position) {
ManagerDetailsFragment detailFragment = (ManagerDetailsFragment) getFragmentManager().findFragmentById(R.id.fragmentDetailManager);
if (detailFragment != null && detailFragment.isInLayout()) {
// fragment available, do our stuff in here.
if(position == selectionPosition)
return;
ManagerListFragment managerListFragment = (ManagerListFragment) getFragmentManager().findFragmentById(R.id.fragmentModuleList);
if(managerListFragment != null) {
detailFragment.setId(shownModuleList.get(position).getId());
selectionPosition = position;
detailFragment.update();
}
} else {
// fragment unavailable, launch new activity.
Intent intent = new Intent(getApplicationContext(), ManagerDetailActivity.class);
ManagerListFragment managerListFragment = (ManagerListFragment) getFragmentManager().findFragmentById(R.id.fragmentModuleList);
if(managerListFragment != null){
intent.putExtra(ManagerDetailActivity.EXTRA_MODULE_ID, shownModuleList.get(position).getId());
startActivity(intent);
} else
Log.d("DEBUG", "managerListFragment == null");
}
}
/**
* Defer onClick() event to {@link pk.contender.earmouse.ManagerDetailsFragment} (tablet only)
* @param v The view that was clicked
*/
public void onButtonClick(View v) {
ManagerDetailsFragment detailFragment = (ManagerDetailsFragment) getFragmentManager().findFragmentById(R.id.fragmentDetailManager);
if(detailFragment != null && detailFragment.isInLayout()) {
detailFragment.onButtonClick(v);
selectionPosition = -1;
detailFragment.update();
} else
Log.d("DEBUG", "Could not relay click event");
}
/**
* Downloads and installs all the modules in {@link #selection}
*/
private class fetchAndInstallSelection extends AsyncTask<Void, Void, Integer> {
private Context mCtx;
private List<Module> removals;
@Override
protected Integer doInBackground(Void... params) {
Integer installed = 0;
for(Module mod : selection) {
if(mod == null)
continue;
HttpURLConnection urlConn = null;
URL url = null;
String localizedModulePath = "module" + (Main.getLocaleSuffix().equals("") ? "_" : Main.getLocaleSuffix()) +
"_" + mod.getId() + ".json";
try {
url = new URL(Main.generateModuleUrl(getApplicationContext()) + localizedModulePath);
} catch (MalformedURLException e) {
e.printStackTrace();
cancel(false);
}
try {
assert url != null;
urlConn = (HttpURLConnection) url.openConnection();
// Limit the time the user sits and waits for a malformed custom URL to timeout
urlConn.setConnectTimeout(5000);
} catch (IOException e) {
e.printStackTrace();
cancel(false);
}
InputStreamReader reader = null;
try {
assert urlConn != null;
reader = new InputStreamReader(urlConn.getInputStream());
Module result = new Module(mCtx, reader);
// TODO: If an error occurred during the read, this module should not be written to disk ...
if(result.writeModuleToJson()) {
installed++;
for (Module listMod : shownModuleList) {
if (listMod.getId() == mod.getId()) {
// We want to remove the items from the adapter straight away but have to do this on the UI thread.
removals.add(listMod);
break;
}
}
}
reader.close();
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
cancel(false);
return installed;
} finally {
assert urlConn != null;
urlConn.disconnect();
}
}
return installed;
}
@Override
protected void onPreExecute() {
mCtx = getApplicationContext();
removals = new ArrayList<>(selection.size());
}
@Override
protected void onPostExecute(Integer installed) {
// update ListView
for(Module mod : removals)
mAdapter.remove(mod);
// FIXME: Returning to main before installation is completed will not update ListView in Main with new modules.
// Inform user of amount of successfully installed modules
Resources res = mCtx.getResources();
String s = installed + " " + res.getQuantityString(R.plurals.plural_module, installed) + " " + getString(R.string.cab_installed);
Toast toast = Toast.makeText(mCtx, s, Toast.LENGTH_LONG);
toast.show();
// update detailfragment
ManagerDetailsFragment detailFragment = (ManagerDetailsFragment) getFragmentManager().findFragmentById(R.id.fragmentDetailManager);
if(detailFragment != null && detailFragment.isInLayout()) {
detailFragment.setId(-1); // this will set detailfragment to empty view
detailFragment.update();
}
}
@Override
protected void onCancelled(Integer integer) {
Toast toast = Toast.makeText(mCtx, mCtx.getString(R.string.toast_error_installing_module), Toast.LENGTH_LONG);
toast.show();
}
}
/**
* Downloads a list of available modules and displays its contents, minus the modules that are already installed,
* in {@link pk.contender.earmouse.ManagerListFragment}
* @author Paul Klinkenberg <pklinken.development@gmail.com>
*/
private class FetchListJsonFromServer extends AsyncTask<Void, Void, List<Module>> {
//private AndroidHttpClient httpClient = null;
private Context mCtx = null;
@Override
protected void onPreExecute() {
mCtx = getApplicationContext();
}
@Override
protected void onPostExecute(List<Module> result) {
if(result == null) {
Toast toast = Toast.makeText(mCtx, mCtx.getResources().getText(R.string.http_received_empty), Toast.LENGTH_LONG);
toast.show();
return;
}
List<Integer> idsToRemove = new ArrayList<>();
for(Module mod : shownModuleList)
idsToRemove.add(mod.getId());
// idsToRemove now contains all the Module IDS that are already shown in the list so need not be added.
Module modLocal;
for(Module mod : result) {
if (!idsToRemove.contains(mod.getId())) {
if ((modLocal = Main.getModuleById(mod.getId())) == null || modLocal.getModuleVersion() < mod.getModuleVersion()) {
shownModuleList.add(mod);
}
}
}
Collections.sort(shownModuleList);
ManagerListFragment managerListFragment = (ManagerListFragment) getFragmentManager().findFragmentById(R.id.fragmentModuleList);
if(managerListFragment != null){
managerListFragment.setListShown(true);
}
mAdapter.notifyDataSetChanged();
}
@Override
protected void onCancelled(List<Module> result) {
// Task was cancelled as there was an error contacting server, show only an empty list and a Toast error message
if(mCtx != null) {
Toast toast = Toast.makeText(mCtx, mCtx.getResources().getText(R.string.http_error), Toast.LENGTH_LONG);
toast.show();
shownModuleList.clear(); // just show an empty list
ManagerListFragment managerListFragment = (ManagerListFragment) getFragmentManager().findFragmentById(R.id.fragmentModuleList);
if(managerListFragment != null){
managerListFragment.setListShown(true);
}
mAdapter.notifyDataSetChanged();
}
// else: Calling activity was destroyed before AsyncTask completed, do nothing.
}
@Override
protected List<Module> doInBackground(Void... params) {
HttpURLConnection urlConn = null;
URL url = null;
String localizedModulePath = "list" + Main.getLocaleSuffix() + ".json";
try {
url = new URL(Main.generateModuleUrl(getApplicationContext()) + localizedModulePath);
} catch (MalformedURLException e) {
e.printStackTrace();
cancel(false);
}
try {
assert url != null;
urlConn = (HttpURLConnection) url.openConnection();
// Limit the time the user sits and waits for a malformed custom URL to timeout
urlConn.setConnectTimeout(5000);
} catch (IOException e) {
e.printStackTrace();
cancel(false);
}
InputStreamReader reader;
List<Module> moduleList;
try {
assert urlConn != null;
reader = new InputStreamReader(urlConn.getInputStream());
moduleList = readListFromJson(reader);
} catch (IllegalStateException | IOException e) {
e.printStackTrace();
cancel(false);
return null;
} finally {
assert urlConn != null;
urlConn.disconnect();
}
return moduleList;
}
/*
* Note that the JSON names here are different, because the python variable naming convention ended up being the JSON naming
* so shortDescription is short_description etc.
*/
private List<Module> readListFromJson(InputStreamReader in) throws IOException {
JsonReader reader = new JsonReader(in);
List<Module> serverModuleList = new ArrayList<>();
reader.beginArray();
while (reader.hasNext()) {
reader.beginObject();
Module mod = new Module(mCtx);
while(reader.hasNext()) {
String name = reader.nextName();
switch (name) {
case "module_id":
mod.setId(reader.nextInt());
break;
case "module_title":
mod.setTitle(reader.nextString());
break;
case "difficulty":
mod.setDifficulty(reader.nextInt());
break;
case "short_description":
mod.setShortDescription(reader.nextString());
break;
case "module_version":
mod.setModuleVersion(reader.nextInt());
break;
default:
reader.skipValue();
break;
}
}
serverModuleList.add(mod);
reader.endObject();
}
reader.endArray();
reader.close();
in.close();
Log.d("DEBUG", "serverModuleList.size() returns " + serverModuleList.size());
return serverModuleList;
}
}
}