Java tutorial
/* * Copyright (c) 2015 Thierry Bzecourt * * This file is part of HangulDrill. * * HangulDrill is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * HangulDrill is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with HangulDrill. If not, see <http://www.gnu.org/licenses/>. */ package org.thbz.hanguldrill; import android.app.AlertDialog; import android.content.Context; import android.content.Intent; import android.content.res.AssetManager; import android.os.Build; import android.preference.PreferenceManager; import android.support.v4.app.DialogFragment; import android.support.v7.app.ActionBar; import android.support.v7.app.ActionBarActivity; import android.os.Bundle; import android.util.Log; import android.view.Gravity; import android.view.Menu; import android.view.MenuItem; import android.view.SubMenu; import android.view.View; import android.view.animation.Animation; import android.view.animation.AnimationUtils; import android.widget.Button; import android.widget.ProgressBar; import android.widget.TextView; import android.widget.Toast; import java.util.ArrayList; import java.util.List; import java.util.Set; public class MainActivity extends ActionBarActivity implements ConfigManageDialogFragment.NoticeDialogListener, InputConfigNameDialogFragment.NoticeDialogListener { private final static String TAG = MainActivity.class.getName(); private TextView textView = null; private ProgressBar progressBar = null; private Button mainButton = null; // Gestion des tats : stopped, running... private enum State { STOPPED, RUNNING, ASKSTOP } private State _state; // Implmentation d'un singleton pour obtenir une rfrence l'activit // Ide : si l'activit est dtruite et reconstruite, je suppose que onCreate() est // nouveau appel et que s_instance est donc recre. // Donc il faut sans doute mieux appeler getInstance() chaque fois qu'on en a besoin // plutt que de stocker une rfrence. Mais il est sans doute prfrable de passer le // contexte en argument aux fonctions chaque fois que possible. static private MainActivity s_instance; synchronized static private MainActivity getInstance() { return s_instance; // La valeur devra tre teste avant d'tre utilise } static Context doGetApplicationContext() { MainActivity main = getInstance(); if (main == null) return null; else return main.getApplicationContext(); } synchronized static void setInstance(MainActivity mainActivity) { s_instance = mainActivity; } // Accs l'tat protected synchronized State getState() { // debug("getState = " + _state); return _state; } protected synchronized void setState(State state) { _state = state; if (_state == State.STOPPED) { updateMainButtonText(getString(R.string.main_button_start)); } else if (_state == State.RUNNING) updateMainButtonText(getString(R.string.main_button_stop)); } /* private void getOverflowMenu() { try { ViewConfiguration config = ViewConfiguration.get(this); Field menuKeyField = ViewConfiguration.class.getDeclaredField("sHasPermanentMenuKey"); if(menuKeyField != null) { menuKeyField.setAccessible(true); menuKeyField.setBoolean(config, false); } } catch (Exception e) { e.printStackTrace(); } } */ // Initialisations @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); // Empcher des appels this pendant qu'il n'est pas entirement construit setInstance(null); setState(State.STOPPED); // Rcupration ou cration des prfrences try { Settings.Configuration lastSelectedConfig = Settings.ConfigManager.getLastSelectedConfig(this); if (lastSelectedConfig == null) resetConfigurations(); } catch (Throwable thro) { if (BuildConfig.DEBUG) if (BuildConfig.DEBUG) Log.e(TAG, "Throwable while loading the preferences", thro); } setContentView(R.layout.activity_main); textView = (TextView) findViewById(R.id.text); progressBar = (ProgressBar) findViewById(R.id.progressBar); progressBar.setVisibility(ProgressBar.INVISIBLE); mainButton = (Button) findViewById(R.id.mainButton); ActionBar actionBar = getSupportActionBar(); actionBar.setIcon(R.drawable.ic_launcher); updateTextSizeFromSettings(); updateThemeFromSettings(); setInstance(this); // Lance l'initialisation de la base // WordDbContract.ensureDatabaseInitialized(this); try { WordDbContract.DbHelper.setupDbIfNeeded(getApplicationContext()); } catch (InternalException exc) { toastError(exc.getMessage()); } /* // Hack : see http://stackoverflow.com/questions/9739498/android-action-bar-not-showing-overflow getOverflowMenu(); */ } // Arrt de l'animation en cas de pause de l'activit protected void onPause() { super.onPause(); if (getState() == State.RUNNING) setState(State.ASKSTOP); } protected void onDestroy() { super.onDestroy(); setInstance(null); } // Gestion des configurations private final int START_MENUITEM_ID = 1; // Initialisation du menu des configurations dans l'Option Menu @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_activity_actions, menu); try { MenuItem configListItem = menu.findItem(R.id.action_configlist); String[] configIds = Settings.ConfigManager.getAllConfigIds(this); if (configListItem.hasSubMenu() && configIds.length > 0) { SubMenu configMenu = configListItem.getSubMenu(); if (configMenu == null) { if (BuildConfig.DEBUG) Log.d(TAG, "configMenu = null"); } else { // On ajoute une entre pour chacune des configurations existantes for (int i = 0; i < configIds.length; i++) { String configId = configIds[i]; Settings.Configuration config = Settings.ConfigManager.getConfigFromId(this, configId); String name = config.getName(this); configMenu.add(Menu.NONE, START_MENUITEM_ID + i, i, name); } } // Set configMenu title try { Settings.Configuration lastSelectedConfig = Settings.ConfigManager.getLastSelectedConfig(this); if (lastSelectedConfig != null) { String configName = lastSelectedConfig.getName(this); if (configName != null) { if (configName.length() > 15) configName = configName.substring(0, 13) + "\u2026"; configListItem.setTitle(configName); } } } catch (ClassCastException exc) { toastError("Exception : " + exc.getMessage()); } } } catch (InternalException exc) { if (BuildConfig.DEBUG) if (BuildConfig.DEBUG) Log.e(TAG, "Erreur interne", exc); } return super.onCreateOptionsMenu(menu); } // Implmentation de InputConfigNameDialogFragment.NoticeDialogListener // Un nom de configuration a t entr dans un objet InputConfigNameDialogFragment // pour enregistrer les paramtres actuels. @Override public void onInputConfigName(String newConfigName) { try { String[] configIds = Settings.ConfigManager.getAllConfigIds(this); for (String configId : configIds) { Settings.Configuration config = Settings.ConfigManager.getConfigFromId(this, configId); if (config.getName(this).equals(newConfigName)) { alert("A configuration already exists with this name. Please choose another name."); return; } } Settings.Configuration newConfig = Settings.ConfigManager.saveSettingsAsConfig(this, newConfigName); if (newConfig != null) { // Redraw the menu if necessary if (Build.VERSION.SDK_INT >= 11) invalidateOptionsMenu(); // Message de confirmation toastMessage("Config saved as " + newConfigName); if (BuildConfig.DEBUG) Log.d(TAG, "Config copie vers " + newConfig.getId()); } } catch (InternalException exc) { if (BuildConfig.DEBUG) Log.e(TAG, "Internal error", exc); } } @Override public boolean onOptionsItemSelected(MenuItem item) { try { int countConfigs = Settings.ConfigManager.getAllConfigIds(this).length; int itemId = item.getItemId(); if (itemId == R.id.action_configure_savecurrentsettings) { InputConfigNameDialogFragment dialog = new InputConfigNameDialogFragment(); dialog.setListener(this); dialog.show(getSupportFragmentManager(), "InputTextDialogFragment"); } else if (itemId == R.id.action_configure_manageconfigs) { ConfigManageDialogFragment configManageDialog = new ConfigManageDialogFragment(); configManageDialog.show(getSupportFragmentManager(), "configManage"); } else if (itemId == R.id.action_configure_reinitconfigs) { ConfirmDialogFragment.Listener listener = new ConfirmDialogFragment.Listener() { @Override public void onConfirmDialogYes() { resetConfigurations(); synchronized (this) { if (getState() == State.RUNNING) setState(State.ASKSTOP); } toastMessage("Configurations have been reinitialized"); } @Override public void onConfirmDialogNo() { toastMessage("Configurations have not been modified"); } }; ConfirmDialogFragment dialog = new ConfirmDialogFragment(); dialog.setMessage("Do you really want to reinitialize the configurations?").setListener(listener) .show(getSupportFragmentManager(), "ConfirmDialogFragment"); } else if (itemId == R.id.action_configure_help) { DialogFragment helpDialog = new HelpDialogFragment(); helpDialog.show(getSupportFragmentManager(), "helpDialog"); } /* else if(itemId == R.id.action_configure_resetdb) { ConfirmDialogFragment.Listener listener = new ConfirmDialogFragment.Listener() { @Override public void onConfirmDialogYes() { try { if (getState() == State.RUNNING) { MainActivity.doToastError("You must first press the Stop button"); return; } WordDbContract.DbHelper.resetDb(MainActivity.this); toastMessage("Database has been reset."); } catch(InternalException exc) { toastError("Error: " + exc.getMessage()); } } @Override public void onConfirmDialogNo() { toastMessage("Cancelled."); } }; ConfirmDialogFragment dialog = new ConfirmDialogFragment(); dialog.setMessage("Do you really want to reset the database?") .setListener(listener) .show(getSupportFragmentManager(), "ConfirmDialogFragment"); } */ else if (itemId >= START_MENUITEM_ID && itemId < START_MENUITEM_ID + countConfigs) { // Select one of the existing configurations String[] configIds = Settings.ConfigManager.getAllConfigIds(this); String configId = configIds[itemId - START_MENUITEM_ID]; Settings.Configuration config = Settings.ConfigManager.getConfigFromId(this, configId); config.saveAsSettings(this); if (Build.VERSION.SDK_INT >= 11) invalidateOptionsMenu(); if (BuildConfig.DEBUG) Log.d(TAG, "selected " + configId); return true; } else if (itemId == R.id.action_settings) { // Bouton ouvrant le menu des paramtres synchronized (this) { if (getState() == State.RUNNING) setState(State.ASKSTOP); } openSettings(); return true; } } catch (InternalException exc) { if (BuildConfig.DEBUG) Log.e(TAG, "Internal error", exc); } return super.onOptionsItemSelected(item); } public void openSettings() { Intent intent = new Intent(this, SettingsActivity.class); startActivity(intent); } protected void resetConfigurations() { try { Settings.ConfigManager.resetConfigurations(this); if (Build.VERSION.SDK_INT >= 11) invalidateOptionsMenu(); } catch (InternalException exc) { toastError("Reset has failed: " + exc.getMessage()); if (BuildConfig.DEBUG) Log.e(TAG, "Internal error", exc); } } // Utilitaires // A appeler si les settings ont t modifis protected void updateTextSizeFromSettings() { String sizeSetting = PreferenceManager.getDefaultSharedPreferences(this) .getString(Settings.Key.pref_textSize, "30"); try { final float textSize = Float.parseFloat(sizeSetting); runOnUiThread(new Runnable() { public void run() { textView.setTextSize(textSize); } }); } catch (NumberFormatException exc) { MainActivity.doToastError(exc.getMessage()); } } static synchronized void doUpdateTextSizeFromSettings() { MainActivity main = getInstance(); if (main != null) main.updateTextSizeFromSettings(); } protected void updateThemeFromSettings() { String themeName = PreferenceManager.getDefaultSharedPreferences(this).getString(Settings.Key.pref_theme, "black/white"); final Settings.Theme theme = Settings.Theme.get(themeName); runOnUiThread(new Runnable() { public void run() { textView.setTextColor(theme.textColor); View mainView = findViewById(R.id.main_layout); mainView.setBackgroundColor(theme.backgroundColor); } }); } static synchronized void doUpdateThemeFromSettings() { MainActivity main = getInstance(); if (main != null) main.updateThemeFromSettings(); } // Ncessaire pour modifier des lments de l'UI depuis un autre thread public void updateText(final String text) { runOnUiThread(new Runnable() { public void run() { if (textView != null) textView.setText(text); } }); } private void updateMainButtonText(final String text) { runOnUiThread(new Runnable() { public void run() { if (mainButton != null) mainButton.setText(text); } }); } // Affiche une alerte (qui requiert l'attention de l'utilisateur, par opposition // au toastMessage, qui peut tre ignor protected void alert(final String text) { new AlertDialog.Builder(this).setTitle("Warning").setMessage(text) .setPositiveButton(android.R.string.yes, null).setIcon(android.R.drawable.ic_dialog_alert).show(); } // Affiche un message qui disparat au bout de quelques secondes // (par exemple pour confirmer qu'une action a t effectue) private void toastMessage(final String text) { runOnUiThread(new Runnable() { public void run() { Context context = getApplicationContext(); Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT); toast.show(); } }); } static synchronized public void doToastMessage(final String text) { MainActivity main = getInstance(); if (main != null) main.toastMessage(text); } // Affiche un message qui disparat au bout de quelques secondes // (par exemple pour confirmer qu'une action a t effectue) private void toastError(final String text) { runOnUiThread(new Runnable() { public void run() { Context context = getApplicationContext(); Toast toast = Toast.makeText(context, text, Toast.LENGTH_SHORT); toast.setGravity(Gravity.CENTER, 0, 0); toast.show(); } }); } static synchronized public void doToastError(final String text) { MainActivity main = getInstance(); if (main != null) main.toastError(text); } static synchronized public AssetManager doGetAssets() throws InternalException { MainActivity main = getInstance(); if (main != null) return main.getAssets(); else throw new InternalException("Main acitvity not initialized"); } // Gestion du flux temporel de l'interface utilisateur : clics, progress bar, affichage des // nombres et mots... private void updateProgressBar(final int value) { runOnUiThread(new Runnable() { public void run() { if (progressBar != null) { progressBar.setProgress(value); // Apparition de la progress bar seulement vers la fin if (value >= 75) { if (progressBar.getVisibility() == ProgressBar.INVISIBLE) { // Set visibility progressBar.setVisibility(ProgressBar.VISIBLE); // Animation Animation anim = AnimationUtils.loadAnimation(MainActivity.this, R.anim.progressbar_appear); progressBar.startAnimation(anim); } } else if (progressBar.getVisibility() == ProgressBar.VISIBLE) progressBar.setVisibility(ProgressBar.INVISIBLE); } } }); } private int totalDelay = 0; private class GetRandResult { final List<String> words; final int nbSyllables; final List<Integer> digits; GetRandResult(List<String> _words, int _nbSyllables, List<Integer> _digits) { words = _words; nbSyllables = _nbSyllables; digits = _digits; } } private GetRandResult getRand(int nbItems, int nbDigits) throws InternalException { try { List<String> words = new ArrayList<>(); int nbSyllables = 0; for (int i = 0; i < nbItems; i++) { String word = WordDbContract.getWord(this); words.add(word); nbSyllables += word.length(); } List<Integer> digits = new ArrayList<>(); for (int i = 0; i < nbDigits; i++) digits.add((int) (Math.random() * 10)); return new GetRandResult(words, nbSyllables, digits); } catch (RuntimeException exc) { if (BuildConfig.DEBUG) Log.e(TAG, "RuntimeException error", exc); throw exc; } } private void doPrintOutput() throws InternalException { int nbWords = Integer.valueOf(Settings.getCurrentSetting(this, Settings.Key.pref_nbWords, getString(R.string.pref_default_nbWords))); int nbDigits = Integer.valueOf(Settings.getCurrentSetting(this, Settings.Key.pref_nbDigits, getString(R.string.pref_default_nbDigits))); int speed = Integer.valueOf( Settings.getCurrentSetting(this, Settings.Key.pref_speed, getString(R.string.pref_default_speed))); int delaySyllable = (int) Math.floor(2000 / speed); int delayDigit = (int) Math.floor(2500 / speed); GetRandResult res = getRand(nbWords, nbDigits); String chaine = ""; for (int i = 0; i < res.words.size(); i++) { if (i > 0) chaine += " "; chaine += res.words.get(i); } if (res.words.size() > 0 && res.digits.size() > 0) chaine += System.getProperty("line.separator"); int nbD = res.digits.size(); for (int i = 0; i < nbD; i++) { if (i % 8 == 0 && i > 0) chaine += System.getProperty("line.separator"); else if (i < (nbD - (nbD % 8))) { if (i % 4 == 0 && i > 0) chaine += " "; } else if ((nbD - i) % 4 == 0 && i > 0) chaine += " "; chaine += res.digits.get(i); } totalDelay = res.nbSyllables * delaySyllable + res.digits.size() * delayDigit; // Test // chaine = WordDbContract.getTestData(this); updateText(chaine); } private void waitForDelay() { updateProgressBar(0); try { float delayDone = 0; int unit = 25; while (getState() == State.RUNNING && delayDone < totalDelay) { Thread.sleep(unit); delayDone += unit; updateProgressBar((int) Math.floor(100 * delayDone / totalDelay)); } } catch (InterruptedException e) { if (BuildConfig.DEBUG) Log.e(TAG, "ProgressBar thread interrupted", e); } } Thread runningThread = null; public void onClickMainButton(View v) { final State state = getState(); if (state == State.STOPPED) { runningThread = new Thread(new Runnable() { public void run() { try { // WordDbContract.DbHelper.setupDbIfNeeded(getApplicationContext()); while (getState() == State.RUNNING) { doPrintOutput(); waitForDelay(); } // On a t stopp if (getState() == State.ASKSTOP) setState(State.STOPPED); } catch (Exception exc) { if (BuildConfig.DEBUG) Log.e(TAG, "Erreur dans boucle du main button", exc); toastError("Error: " + exc.getMessage()); setState(State.STOPPED); } } }); setState(State.RUNNING); runningThread.start(); } else if (state == State.RUNNING) { setState(State.ASKSTOP); try { if (runningThread == null) throw new Exception("Error : no current action to stop!"); runningThread.join(); } catch (InterruptedException ie) { if (BuildConfig.DEBUG) Log.e(TAG, "Main thread interrupted", ie); } catch (Exception e) { if (BuildConfig.DEBUG) Log.e(TAG, "Exception", e); } } // else state = ASKSTOP (double clic ?) : on ne fait rien } // Implementation de ConfigManageDialogFragment.NoticeDialogListener : destruction de // configurations @Override public void onConfigManageSaveClick(Set<Integer> configToDeleteIds) { try { boolean lastSelectedConfigIsDestroyed = false; Settings.Configuration lastSelectedConfig = Settings.ConfigManager.getLastSelectedConfig(this); String lastSelectedConfigId = (lastSelectedConfig == null ? null : lastSelectedConfig.getId()); List<String> allConfigIds = Settings.ConfigManager.getAllConfigIdsAsList(this); List<String> newAllConfigIds = new ArrayList<>(); int nbDone = 0; for (int i = 0; i < allConfigIds.size(); i++) { String configId = allConfigIds.get(i); if (configToDeleteIds.contains(i)) { Settings.ConfigManager.deleteConfigFromId(this, configId); nbDone++; if (lastSelectedConfigId != null && configId.equals(lastSelectedConfigId)) lastSelectedConfigIsDestroyed = true; } else newAllConfigIds.add(configId); } // Enregistrer la liste des ids de configuration Settings.ConfigManager.setAllConfigIds(this, newAllConfigIds); // Si la configuration courante a t dtruite, on slectionne // la premire configuration de la liste if (lastSelectedConfigIsDestroyed) { if (newAllConfigIds.size() >= 1) { String firstConfigId = newAllConfigIds.get(0); Settings.Configuration config = Settings.ConfigManager.getConfigFromId(this, firstConfigId); config.saveAsSettings(this); } } if (Build.VERSION.SDK_INT >= 11) invalidateOptionsMenu(); toastMessage("Deleted " + nbDone + " configuration" + (nbDone > 1 ? "s" : "")); } catch (InternalException exc) { toastError("Erreur : " + exc.getMessage()); } } public void onConfigManageException(InternalException exc) { toastError(exc.getMessage()); } }