package pk.contender.earmouse; import android.app.ActionBar; import android.app.Activity; import android.app.AlertDialog; import android.app.Dialog; import android.app.DialogFragment; import android.content.Context; import android.content.Intent; import android.content.SharedPreferences; import android.content.res.AssetManager; import android.content.res.Resources; import android.media.AudioManager; import android.os.Bundle; import android.os.StrictMode; import android.support.annotation.Nullable; import android.text.Html; import android.text.Spanned; import android.text.method.LinkMovementMethod; import android.util.Log; import android.view.ActionMode; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.Menu; import android.view.MenuInflater; import android.view.MenuItem; import android.view.View; import android.widget.AbsListView; import android.widget.ListView; import android.widget.TextView; import android.widget.Toast; import java.io.File; import java.io.FileOutputStream; import java.io.FilenameFilter; import java.io.IOException; import java.io.InputStream; import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.Locale; /** * Main Activity class, starting point for the App and implementation of * the Main activity. * * Also has code that will install a given set of Modules on the first * launch of the app. * * @author Paul Klinkenberg <pklinken.development@gmail.com> */ public class Main extends Activity implements ModuleListFragment.OnModuleSelectedListener, ButtonGridFragment.AnswerSelectedListener, ButtonGridFragment.PracticeModeToggleListener, ConfirmDeleteDialog.ConfirmDeleteDialogListener, ConfirmResetDialog.ConfirmResetDialogListener { // TODO: Remember scroll position /** Version string, only used in About dialog. Should always match version in Manifest. */ public static final String VERSION = "v1.2"; /** The default server address to use for fetching remote data */ public static final String SERVER_HOST = "pklinken.github.io"; /** The default server port to use for fetching remote data */ public static final int SERVER_PORT = 80; /** The default path on the server where the data can be found */ public static final String SERVER_PATH = "/Earmouse/Earmouse_localized/"; /** The full default url */ public static final String SERVER_URL = "http://" + SERVER_HOST + ":" + SERVER_PORT + SERVER_PATH; /** Available locales */ public static final String[] SUPPORTED_LOCALES = { "de" }; /** Set to true to enable StrictMode testing */ private static final boolean DEVELOPER_MODE = false; /** The index of the currently selected entry in the ModuleListFragment */ //private int selectionIndex; /* SharedPreferences constants */ private static final String PREFS_FIRSTLAUNCH = "prefs_firstlaunch"; public static final String PREFS_FASTPLAY = "prefs_fastplay"; //private static final String PREFS_SELECTIONINDEX = "prefs_main_selectionindex"; public static final String PREFS_NAME = "EarmousePrefs"; private static final String PREFS_USE_CUSTOM_HOST = "prefs_custom_host_toggle"; private static final String PREFS_CUSTOM_HOSTNAME = "prefs_custom_host"; private static final String PREFS_CUSTOM_PATH = "prefs_custom_path"; private static final String PREFS_CUSTOM_PORT = "prefs_custom_port"; /** * reference to the Activity's ActionMode, is null if the Activity is not in ActionMode */ private ActionMode mActionMode; /** * when in ActionMode, this List contains all the Modules the user currently has * selected. */ private List<Module> selection = null; /** * Lists all the Modules currently installed, used by the Main Activity's ListView * adapter. */ private static List<Module> mModules; /** * Adapter used by the Main Activity's ListView. */ private ModuleListAdapter mAdapter; /** * Dialog currently being shown or null if none. * Only used for the About dialog, not the ActionMode DialogFragments */ private Dialog mDialog; /** * Loads saved state and preferences, installs default package of modules if this is the * first time the App runs, ties the volume controls to the media stream that we use * and sets up the context actionbar (CAB). * @param savedInstanceState the previously saved instance state, can be null. */ @Override protected void onCreate(final Bundle savedInstanceState) { if (DEVELOPER_MODE) { StrictMode.setThreadPolicy(new StrictMode.ThreadPolicy.Builder() .detectDiskReads() .detectDiskWrites() .detectNetwork() // or .detectAll() for all detectable problems .penaltyLog() .penaltyFlashScreen() .build()); StrictMode.setVmPolicy(new StrictMode.VmPolicy.Builder() .detectLeakedSqlLiteObjects() .detectLeakedClosableObjects() .penaltyLog() .penaltyDeath() .build()); } super.onCreate(savedInstanceState); SharedPreferences settings = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); boolean firstLaunch = settings.getBoolean(PREFS_FIRSTLAUNCH, true); //selectionIndex = settings.getInt(PREFS_SELECTIONINDEX, -2); if(firstLaunch) { // This is the first time the App runs, install the default modules and set up empty fragment view Log.d("DEBUG", "First launch of app"); installDefaultModules(); settings.edit().putBoolean(PREFS_FIRSTLAUNCH, false).apply(); } refreshModuleList(this); setContentView(R.layout.activity_main); ActionBar actionBar = getActionBar(); if(actionBar != null) // We do use the UP navigation, just not in the Main activity actionBar.setDisplayHomeAsUpEnabled(false); // Set the in-app volume control to always control the stream we use for playback. setVolumeControlStream(AudioManager.STREAM_MUSIC); // Set up the Context ActionMode and the mAdapter field ListView lv = null; ModuleListFragment moduleListFragment = (ModuleListFragment) getFragmentManager().findFragmentById(R.id.fragment_modulelist); if(moduleListFragment != null){ lv = moduleListFragment.getListView(); mAdapter = (ModuleListAdapter) moduleListFragment.getListAdapter(); } 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 SuspiciousMethodCalls selection.remove(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.main_ctx_delete: DialogFragment dialog = new ConfirmDeleteDialog(); dialog.show(getFragmentManager(), "confirmDeleteDialog"); return true; case R.id.main_ctx_reset: DialogFragment dialog2 = new ConfirmResetDialog(); dialog2.show(getFragmentManager(), "confirmResetDialog"); 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.main_context, menu); mActionMode = mode; selection = new ArrayList<>(); 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 e.g. 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) { // Here you can make any necessary updates to the activity when // the CAB is removed. By default, selected items are deselected/unchecked. 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; } }); } } /** * Close any open Dialogs to prevent window leak. * Does not appear to be necessary for ActionMode dialogs. */ @Override public void onPause() { super.onPause(); if(mDialog != null) mDialog.dismiss(); //SharedPreferences settings = getSharedPreferences(PREFS_NAME, MODE_PRIVATE); //settings.edit().putInt(PREFS_SELECTIONINDEX, selectionIndex).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); } @Override public boolean onCreateOptionsMenu(Menu menu) { // Inflate the menu; this adds items to the action bar if it is present. getMenuInflater().inflate(R.menu.main, menu); return true; } /** * Implements the Manage and About Actionbar options. * @param item Menu item that was selected * @return False to allow normal menu processing to proceed, true to consume it here. */ @Override public boolean onOptionsItemSelected(MenuItem item) { // Handle action bar item clicks here. The action bar will // automatically handle clicks on the Home/Up button, so long // as you specify a parent activity in AndroidManifest.xml. int id = item.getItemId(); switch(id) { case R.id.action_manage: Intent intent = new Intent(getApplicationContext(), ModuleManagerActivity.class); startActivity(intent); return true; case R.id.action_about: String text = String.format(getResources().getString(R.string.about_message), VERSION); CharSequence styledText = Html.fromHtml(text); final AlertDialog d = new AlertDialog.Builder(this) .setTitle(R.string.about_title) .setMessage(styledText) .setCancelable(true) .create(); mDialog = d; d.show(); // Make text a bit smaller TextView dialogView = (TextView) d.findViewById(android.R.id.message); dialogView.setTextSize(14); // ?? ((TextView) d.findViewById(android.R.id.message)).setMovementMethod(LinkMovementMethod.getInstance()); return true; case R.id.action_prefs: intent = new Intent(); intent.setClass(getApplicationContext(), SettingsActivity.class); startActivity(intent); return true; } return super.onOptionsItemSelected(item); } /** * Listener for the attached ListView, handles item selection. * Depending on the layout, either starts an {@link pk.contender.earmouse.ExerciseActivity} Intent * with the position of the clicked item or updates the {@link pk.contender.earmouse.ExerciseFragment } * to display the item at position. * @param position Position of the list item that was clicked. */ @Override public void onModuleSelected(View view, int position) { ExerciseFragment fragment = (ExerciseFragment) getFragmentManager() .findFragmentById(R.id.fragment_exercise); if (fragment == null || !fragment.isInLayout()) { // fragment unavailable, launch new activity. Intent intent = new Intent(getApplicationContext(), ExerciseActivity.class); intent.putExtra(ExerciseActivity.EXTRA_POSITION, position); SharedPreferences settings = getSharedPreferences(Main.PREFS_NAME, Activity.MODE_PRIVATE); settings.edit().putBoolean(ExerciseFragment.PREFERENCES_ISFRESHINTENT, true).apply(); startActivity(intent); } else { // fragment available, do our stuff in here. if (fragment.getModuleIndex() != position) { //selectionIndex = position; fragment.setModule(position); } } } /** * Loads all the locally installed modules, and returns them as a sorted list. * * @param ctx The context used for the File functions and the Module constructor. * @return A List<Module> of all the modules locally installed on the device, * can be empty. */ static private List<Module> loadModulesList(Context ctx) { List<Module> moduleList = new ArrayList<>(); File currentDir = ctx.getDir("files", MODE_PRIVATE); FilenameFilter moduleFilter = new FilenameFilter() { @Override public boolean accept(File dir, String filename) { return filename.startsWith("module" + getLocaleSuffix()); } }; File moduleFileList [] = currentDir.listFiles(moduleFilter); for (File moduleFile : moduleFileList) { moduleList.add(new Module(ctx, moduleFile)); } Collections.sort(moduleList); return moduleList; } /** * Refreshes {@link Main#mModules} to contain all the locally installed modules. * Invalidates the current adapter used for the ListView in {@link pk.contender.earmouse.Main}. */ static public void refreshModuleList(Context ctx) { mModules = loadModulesList(ctx); } /** * Returns the value of {@link Main#mModules}. * @return the value of {@link Main#mModules}. */ static public List<Module> getModuleList() { return mModules; } /** * Returns the module with given id or null if not found * @param id * @return found module or null */ @Nullable static public Module getModuleById(int id) { for(Module mod : mModules) { if (mod.getId() == id) { return mod; } } return null; } /** * Install the default module_*.json files from the assets. * Called only the first time the application runs to provide the user * with a basic selection of Modules. */ private void installDefaultModules() { AssetManager assetMan = getAssets(); String localizedAssetDir = "modules" + getLocaleSuffix(); try { String [] assetList = assetMan.list(localizedAssetDir); File currentDir = getDir("files", Context.MODE_PRIVATE); for(String item : assetList) { InputStream in = assetMan.open(localizedAssetDir + "/" + item, AssetManager.ACCESS_BUFFER); File outputFile = new File(currentDir, item); FileOutputStream out = new FileOutputStream(outputFile); byte [] buf = new byte[1024]; int len; while((len = in.read(buf)) > 0) out.write(buf, 0, len); in.close(); out.close(); } } catch (IOException e) { e.printStackTrace(); Toast.makeText(this,"Error loading default modules", Toast.LENGTH_LONG).show(); } } /** * Defers click event on Play button to the ExerciseFragment (tablets only) * @param view The view that was clicked. */ public void onClickPlay(View view) { ExerciseFragment fragment = (ExerciseFragment) getFragmentManager() .findFragmentById(R.id.fragment_exercise); if(fragment != null && fragment.isInLayout()) { fragment.onClickPlay(view); } } /** * Defers click event on the ButtonGrid to the ExerciseFragment (tablets only) * @param position Position of the button that was clicked. */ @Override public void onAnswerSelected(int position) { ExerciseFragment fragment = (ExerciseFragment) getFragmentManager() .findFragmentById(R.id.fragment_exercise); if(fragment != null && fragment.isInLayout()) { fragment.onAnswerSelected(position); mAdapter.notifyDataSetChanged(); } } @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { super.onCreateContextMenu(menu, v, menuInfo); MenuInflater inflater = getMenuInflater(); inflater.inflate(R.menu.main_context, menu); } /** * Handler for when user confirms to delete the selected modules. * Will purge the selected modules, ask {@link pk.contender.earmouse.ExerciseFragment} to display the * module at index 0 and display a Toast indicating the successful removal. * @param dialog The dialog that was clicked will be passed into the method. */ @Override public void onDeleteConfirm(DialogFragment dialog) { int deleted = 0; for (Module mod : selection) { if(mod != null) { mAdapter.remove(mod); mod.purgeModule(); deleted++; } } //selectionIndex = 0; ExerciseFragment fragment = (ExerciseFragment) getFragmentManager() .findFragmentById(R.id.fragment_exercise); if (fragment != null && fragment.isInLayout()) { fragment.setModule(0); } //mAdapter.notifyDataSetChanged(); Resources res = getResources(); String s = deleted + " " + res.getQuantityString(R.plurals.plural_module, deleted) + " " + getString(R.string.cab_deleted); Toast.makeText(this, s, Toast.LENGTH_LONG).show(); mActionMode.finish(); // Action picked, so close the CAB } /** * Handler for when the user aborts to delete the selected modules. * @param dialog The dialog that was clicked will be passed into the method. */ @Override public void onDeleteAbort(DialogFragment dialog) { mActionMode.finish(); // Action picked, so close the CAB } /** * Handler for when user confirms to reset the selected modules. * Resets the history of the selected modules and updates {@link pk.contender.earmouse.ModuleListFragment} * and {@link pk.contender.earmouse.ExerciseFragment} and displays a Toast indicating the successful history reset. * @param dialog The dialog that was clicked will be passed into the method. */ @Override public void onResetConfirm(DialogFragment dialog) { int reset = 0; for(Module mod : selection) { if(mod != null) { mod.resetStats(); reset++; } } mAdapter.notifyDataSetChanged(); ExerciseFragment fragment = (ExerciseFragment) getFragmentManager().findFragmentById(R.id.fragment_exercise); if (fragment != null && fragment.isInLayout()) { //fragment.setModule(selectionIndex); fragment.updateFeedbackStatistics(); } Resources res = getResources(); String s = reset + " " + res.getQuantityString(R.plurals.plural_module, reset) + " " + getString(R.string.cab_reset); Toast.makeText(this, s, Toast.LENGTH_LONG).show(); mActionMode.finish(); // Action picked, so close the CAB } /** * Test whether the default Locale is supported by the lesson material * @return True if the default locale is supported by the lesson material */ public static boolean isDefaultLocaleSupported() { for(String locale : SUPPORTED_LOCALES) { if(Locale.getDefault().getLanguage().equals(locale)) return true; } return false; } /** * Return a string that can be inserted into a filename to match the default Locale * @return A string to match the default locale or "" if the default locale is not supported */ public static String getLocaleSuffix() { if(isDefaultLocaleSupported()) { return "_" + Locale.getDefault().getLanguage(); } else { return "_en"; } } /** * Handler for when the user aborts to reset the selected modules. * @param dialog The dialog that was clicked will be passed into the method. */ @Override public void onResetAbort(DialogFragment dialog) { mActionMode.finish(); // Action picked, so close the CAB } @Override public void onPracticeModeToggle() { ExerciseFragment fragment = (ExerciseFragment) getFragmentManager().findFragmentById(R.id.fragment_exercise); if(fragment != null && fragment.isInLayout()) { fragment.onPracticeModeToggle(); } } /** * Returns an URL used for installing new Modules based on the user's preferences. * @param ctx The application context required to access the SharedPreferences * @return The default or user-customized URL for installing new Modules */ public static String generateModuleUrl(Context ctx) { SharedPreferences s = ctx.getSharedPreferences(Main.PREFS_NAME, MODE_PRIVATE); if(s.getBoolean(Main.PREFS_USE_CUSTOM_HOST, false)) { String hostname, path, port; hostname = s.getString(Main.PREFS_CUSTOM_HOSTNAME, SERVER_HOST); path = s.getString(Main.PREFS_CUSTOM_PATH, SERVER_PATH); port = s.getString(Main.PREFS_CUSTOM_PORT, String.valueOf(SERVER_PORT)); return "http://" + hostname + ":" + port + path; } else { return "http://" + SERVER_HOST + ":" + SERVER_PORT + SERVER_PATH; } } }