/** * Aedict - an EDICT browser for Android Copyright (C) 2009 Martin Vysny This program 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. This program 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 this program. If not, see <http://www.gnu.org/licenses/>. */ package sk.baka.aedict; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; import sk.baka.aedict.AedictApp.Config; import sk.baka.aedict.dict.DictEntry; import sk.baka.aedict.dict.Edict; import sk.baka.aedict.jlptquiz.QuizActivity; import sk.baka.aedict.kanji.RomanizationEnum; import sk.baka.aedict.util.DictEntryListActions; import sk.baka.aedict.util.ShowRomaji; import sk.baka.autils.AndroidUtils; import sk.baka.autils.DialogUtils; import sk.baka.autils.MiscUtils; import android.app.Activity; import android.app.AlertDialog; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.os.Bundle; import android.view.ContextMenu; import android.view.Menu; import android.view.MenuItem; import android.view.View; import android.view.ViewGroup; import android.view.ViewGroup.LayoutParams; import android.widget.AdapterView; import android.widget.ArrayAdapter; import android.widget.EditText; import android.widget.FrameLayout; import android.widget.ListView; import android.widget.TabHost; import android.widget.TextView; import android.widget.TwoLineListItem; import android.widget.TabHost.TabContentFactory; /** * A simple notepad activity, a simple kanji persistent storage. Allows for * adding/removing of kanji characters and lookup for a kanji combination. * * @author Martin Vysny */ public class NotepadActivity extends Activity implements TabContentFactory { /** * The cached model (a list of edict entries as only the japanese text is * persisted). */ private final Map<Integer, List<DictEntry>> modelCache = new HashMap<Integer, List<DictEntry>>(); private ShowRomaji showRomaji; @Override protected void onRestoreInstanceState(Bundle savedInstanceState) { showRomaji.loadState(savedInstanceState); } @Override protected void onSaveInstanceState(Bundle outState) { showRomaji.saveState(outState); } /** * Expects {@link DictEntry} as a value. Adds given entry to the model list. */ static final String INTENTKEY_ADD_ENTRY = "addEntry"; static final String INTENTKEY_CATEGORY = "category"; public static void addAndLaunch(final Context activity, final DictEntry entry, final int category) { final Intent intent = new Intent(activity, NotepadActivity.class); intent.putExtra(INTENTKEY_ADD_ENTRY, entry); intent.putExtra(INTENTKEY_CATEGORY, category); activity.startActivity(intent); } public static void addAndLaunch(final Activity activity, final DictEntry entry) { final List<String> notepadCategories = AedictApp.getConfig().getNotepadCategories(); if (notepadCategories.size() <= 1) { addAndLaunch(activity, entry, 0); return; } final AlertDialog.Builder builder = new AlertDialog.Builder(activity); builder.setItems(notepadCategories.toArray(new CharSequence[0]), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { addAndLaunch(activity, entry, which); } }); builder.setTitle(R.string.selectCategory); builder.create().show(); } private TabHost getTabHost() { return (TabHost) findViewById(R.id.tabs); } private int getCategoryCount() { final int result = AedictApp.getConfig().getNotepadCategories().size(); if (result > 1) { return result; } return 1; } /** * Returns currently selected category tab. * @return currently selected tab, 0 if no tabs are displayed. */ private int getCurrentCategory() { if (AedictApp.getConfig().getNotepadCategories().size() > 0) { return getTabHost().getCurrentTab(); } return 0; } private final Map<Integer, ListView> tabContents = new HashMap<Integer, ListView>(); public ListView getListView(final int category) { if (AedictApp.getConfig().getNotepadCategories().size() > 0) { return tabContents.get(category); } return (ListView) findViewById(android.R.id.list); } private void initializeListView(final ListView lv, final int category) { final RomanizationEnum romanization = AedictApp.getConfig().getRomanization(); lv.setAdapter(new ArrayAdapter<DictEntry>(this, android.R.layout.simple_list_item_2, getModel(category)) { @Override public View getView(int position, View convertView, ViewGroup parent) { TwoLineListItem view = (TwoLineListItem) convertView; if (view == null) { view = (TwoLineListItem) getLayoutInflater().inflate(android.R.layout.simple_list_item_2, getListView(category), false); } Edict.print(getModel(category).get(position), view, showRomaji.resolveShowRomaji() ? romanization : null); return view; } }); new DictEntryListActions(this, true, false, true, true) { @Override protected void addCustomItems(ContextMenu menu, DictEntry entry, final int itemIndex) { if (AedictApp.getConfig().getNotepadCategories().size() > 1) { menu.add(0, 20, 20, R.string.moveToCategory).setOnMenuItemClickListener(AndroidUtils.safe(NotepadActivity.this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final List<String> notepadCategories = AedictApp.getConfig().getNotepadCategories(); notepadCategories.remove(category); final AlertDialog.Builder builder = new AlertDialog.Builder(NotepadActivity.this); builder.setItems(notepadCategories.toArray(new CharSequence[0]), new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { final int target = which < category ? which : which + 1; final DictEntry e = getModel(category).remove(itemIndex); getModel(target).add(0, e); onModelChanged(category); onModelChanged(target); } }); builder.setTitle(R.string.selectCategory); builder.create().show(); return true; } })); } } @Override protected void onDelete(int itemIndex) { getModel(category).remove(itemIndex); onModelChanged(category); } @Override protected void onDeleteAll() { final int category = getCurrentCategory(); getModel(category).clear(); onModelChanged(category); } }.register(lv); lv.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> arg0, View arg1, int position, long id) { final EditText edit = (EditText) findViewById(R.id.editNotepadSearch); final DictEntry entry = getModel(category).get(position); final String text = edit.getText().toString(); edit.setText(text + entry.getJapanese()); } }); } @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.notepad); showRomaji = new ShowRomaji() { @Override protected void show(boolean romaji) { for (int i = 0; i < getCategoryCount(); i++) { final ListView lv = getListView(i); if (lv != null) { final ArrayAdapter<?> adapter = (ArrayAdapter<?>) lv.getAdapter(); adapter.notifyDataSetChanged(); } } } }; initializeListView((ListView) findViewById(android.R.id.list), 0); findViewById(R.id.btnNotepadSearch).setOnClickListener(new View.OnClickListener() { public void onClick(View v) { MainActivity.launch(NotepadActivity.this, ((TextView)findViewById(R.id.editNotepadSearch)).getText().toString().trim()); } }); final TabHost tabs = getTabHost(); tabs.setup(); updateTabs(); processIntent(); } private void processIntent() { final Intent intent = getIntent(); if (intent.hasExtra(INTENTKEY_ADD_ENTRY)) { final DictEntry e = (DictEntry) intent.getSerializableExtra(INTENTKEY_ADD_ENTRY); final int category = intent.getIntExtra(INTENTKEY_CATEGORY, 0); getModel(category).add(e); final Config cfg = AedictApp.getConfig(); cfg.setNotepadItems(category, getModel(category)); } } /** * Returns the contents of the category, loading it as required. * @param category the category number. * @return category items, never null, may be empty. */ private List<DictEntry> getModel(final int category) { List<DictEntry> model = modelCache.get(category); if (model == null) { model = AedictApp.getConfig().getNotepadItems(category); modelCache.put(category, model); } return model; } @Override protected void onResume() { super.onResume(); updateTabs(); showRomaji.onResume(); } /** * Persists the model to the {@link Config configuration}. Invoked after * each change. */ private void onModelChanged(final int category) { final Config cfg = AedictApp.getConfig(); cfg.setNotepadItems(category, getModel(category)); final ListView lv = getListView(category); // lv may be null if fucking TabHost does not call createTabContent() // for some fucking reason. if (lv != null) { final ArrayAdapter<?> adapter = (ArrayAdapter<?>) lv.getAdapter(); adapter.notifyDataSetChanged(); } } @Override public boolean onPrepareOptionsMenu(Menu menu) { menu.clear(); showRomaji.register(this, menu); final MenuItem item = menu.add(0, 1, 1, R.string.deleteAll); item.setIcon(android.R.drawable.ic_menu_delete); item.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final int category = getCurrentCategory(); getModel(category).clear(); onModelChanged(category); return true; } })); final MenuItem addCategory = menu.add(0, 2, 2, R.string.addCategory); addCategory.setIcon(android.R.drawable.ic_menu_add); addCategory.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final List<String> categories = AedictApp.getConfig().getNotepadCategories(); categories.add("new"); AedictApp.getConfig().setNotepadCategories(categories); final int category = categories.size() - 1; if (category != 0) { getModel(category).clear(); } updateTabs(); return true; } })); if (!AedictApp.getConfig().getNotepadCategories().isEmpty()) { final MenuItem deleteCategory = menu.add(0, 3, 3, R.string.deleteCategory); deleteCategory.setIcon(android.R.drawable.ic_menu_close_clear_cancel); deleteCategory.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final int category = getCurrentCategory(); // Fucking TabHost will throw NullPointerException if we // dare to remove his sweet fucking current tab, thank you // very much. NEVER REMOVE ALL TABS FROM THE STUPID TABHOST getTabHost().setCurrentTab(0); final List<String> categories = AedictApp.getConfig().getNotepadCategories(); if (categories.size() == 1) { // a special case - delete the category but not its // items categories.clear(); AedictApp.getConfig().setNotepadCategories(categories); } else { categories.remove(category); AedictApp.getConfig().setNotepadCategories(categories); AedictApp.getConfig().setNotepadItems(category, new ArrayList<DictEntry>()); for (int i = category; i < categories.size(); i++) { AedictApp.getConfig().setNotepadItems(i, AedictApp.getConfig().getNotepadItems(i + 1)); } modelCache.clear(); } updateTabs(); return true; } })); final MenuItem renameCategory = menu.add(0, 4, 4, R.string.renameCategory); renameCategory.setIcon(android.R.drawable.ic_menu_edit); renameCategory.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final AlertDialog.Builder builder = new AlertDialog.Builder(NotepadActivity.this); final EditText tv = new EditText(NotepadActivity.this); builder.setView(tv); builder.setTitle(R.string.renameCategory); builder.setPositiveButton(R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { final String newName = tv.getText().toString(); if (MiscUtils.isBlank(newName)) { return; } final int current = getCurrentCategory(); getTabHost().setCurrentTab(0); final List<String> categories = AedictApp.getConfig().getNotepadCategories(); categories.set(current, newName); AedictApp.getConfig().setNotepadCategories(categories); updateTabs(); } }); builder.create().show(); return true; } })); } if (!getAllEntries().isEmpty()) { final MenuItem quiz = menu.add(0, 5, 5, R.string.quiz); quiz.setIcon(R.drawable.ic_menu_compose); quiz.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { List<DictEntry> all = getAllEntries(); Collections.shuffle(all); all = new ArrayList<DictEntry>(all.subList(0, Math.min(20, all.size()))); QuizActivity.launch(NotepadActivity.this, all); return true; } })); final MenuItem sendTo = menu.add(0, 6, 6, R.string.sendTo); sendTo.setIcon(android.R.drawable.ic_menu_send); sendTo.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { final StringBuilder sb = new StringBuilder(); final List<String> categories = AedictApp.getConfig().getNotepadCategories(); if (categories.isEmpty()) { categories.add("default"); } for (int i = 0; i < categories.size(); i++) { sb.append('[').append(categories.get(i)).append("]\n"); for (int j = 0; j < getModel(i).size(); j++) { sb.append(getModel(i).get(j).toString()).append('\n'); } } final Intent intent = new Intent(Intent.ACTION_SEND); intent.setType("text/plain"); intent.putExtra(Intent.EXTRA_SUBJECT, "Aedict Notepad"); intent.putExtra(Intent.EXTRA_TEXT, sb.toString()); startActivity(Intent.createChooser(intent, getString(R.string.sendTo))); return true; } })); } final MenuItem backup = menu.add(0, 7, 7, R.string.backup); backup.setIcon(android.R.drawable.ic_menu_save); backup.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { backupNotepad(); new DialogUtils(NotepadActivity.this).showToast(R.string.backupDone); return true; } })); final MenuItem restore = menu.add(0, 8, 8, R.string.restore); restore.setIcon(android.R.drawable.ic_menu_revert); restore.setOnMenuItemClickListener(AndroidUtils.safe(this, new MenuItem.OnMenuItemClickListener() { public boolean onMenuItemClick(MenuItem item) { restoreNotepad(); return true; } })); return true; } /** * Returns entries from all categories. * @return a list of entries, never null, may be empty. */ private List<DictEntry> getAllEntries() { final List<DictEntry> result = new ArrayList<DictEntry>(); for (int i = 0; i < getCategoryCount(); i++) { result.addAll(AedictApp.getConfig().getNotepadItems(i)); } return result; } public View createTabContent(String tag) { final int category = Integer.parseInt(tag); if (category < 0) { throw new IllegalArgumentException("Invalid category value: " + category); } final ListView lv = new ListView(this); lv.setLayoutParams(new FrameLayout.LayoutParams(LayoutParams.FILL_PARENT, LayoutParams.FILL_PARENT)); initializeListView(lv, category); tabContents.put(category, lv); return lv; } /** * Updates all tabs by removing them and re-creating them anew. */ private void updateTabs() { getTabHost().setCurrentTab(0); final TabHost tabs = getTabHost(); tabContents.clear(); tabs.clearAllTabs(); final List<String> categories = AedictApp.getConfig().getNotepadCategories(); findViewById(android.R.id.list).setVisibility(categories.isEmpty() ? View.VISIBLE : View.GONE); tabs.setVisibility(categories.isEmpty() ? View.GONE : View.VISIBLE); if (categories.isEmpty()) { // add a single tab to the TabHost otherwise it will throw // NullPointerException later on. It is amazing how much TabHost // fucking sucks. getTabHost().addTab(getTabHost().newTabSpec("0").setIndicator("0").setContent(this)); initializeListView((ListView) findViewById(android.R.id.list), 0); } int i = 0; for (final String cat : categories) { final TabHost.TabSpec newTab = getTabHost().newTabSpec(Integer.toString(i++)).setIndicator(cat).setContent(this); getTabHost().addTab(newTab); } } private static final File BACKUP = new File("/sdcard/aedict/notepad.backup"); private void backupNotepad() { try { final ObjectOutputStream dos = new ObjectOutputStream(new FileOutputStream(BACKUP)); try { final List<String> categories = AedictApp.getConfig().getNotepadCategories(); dos.writeObject(categories); for (int i = 0; i < categories.size(); i++) { dos.writeObject(AedictApp.getConfig().getNotepadItems(i)); } } finally { MiscUtils.closeQuietly(dos); } } catch (Exception ex) { throw new RuntimeException(ex); } } @SuppressWarnings("unchecked") private void restoreNotepad() { if(!BACKUP.exists()) { new DialogUtils(this).showErrorDialog(R.string.noBackup); return; } try { final List<String> categories; final Map<String, List<DictEntry>> items = new HashMap<String,List<DictEntry>>(); final ObjectInputStream dos = new ObjectInputStream(new FileInputStream(BACKUP)); try { categories = (List<String>) dos.readObject(); for (int i = 0; i < categories.size(); i++) { items.put(categories.get(i), (List<DictEntry>) dos.readObject()); } } finally { MiscUtils.closeQuietly(dos); } final AlertDialog.Builder builder = new AlertDialog.Builder(NotepadActivity.this); builder.setItems(R.array.notepadRestore, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { final boolean isMerge = which == 1; if(isMerge){ final List<String> currentCategories = AedictApp.getConfig().getNotepadCategories(); for (int i=0;i<currentCategories.size();i++){ final String currentCategory = currentCategories.get(i); final List<DictEntry> currentItems = AedictApp.getConfig().getNotepadItems(i); final List<DictEntry> backupItems = items.get(currentCategory); if (backupItems == null) { items.put(currentCategory, currentItems); categories.add(currentCategory); } else { for (final DictEntry e : currentItems) { if (!backupItems.contains(e)) { backupItems.add(e); } } } } } AedictApp.getConfig().setNotepadCategories(categories); for(int i=0;i<categories.size();i++){ AedictApp.getConfig().setNotepadItems(i, items.get(categories.get(i))); } modelCache.clear(); updateTabs(); } }); builder.setTitle(R.string.restoreNotepad); builder.create().show(); } catch (Exception ex) { throw new RuntimeException(ex); } } }