Java tutorial
package info.staticfree.android.units; /* * Units.java * Copyright (C) 2010 Steve Pomeroy <steve@staticfree.info> * * 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/>. */ import info.staticfree.android.units.ValueGui.ConversionException; import info.staticfree.android.units.ValueGui.ReciprocalException; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.util.HashMap; import net.sourceforge.unitsinjava.DefinedFunction; import net.sourceforge.unitsinjava.EvalError; import net.sourceforge.unitsinjava.Function; import net.sourceforge.unitsinjava.Value; import org.jared.commons.ui.WorkspaceView; import android.app.Activity; import android.app.AlertDialog; import android.app.AlertDialog.Builder; import android.app.Dialog; import android.app.ProgressDialog; import android.app.SearchManager; import android.content.ContentUris; import android.content.ContentValues; import android.content.Context; import android.content.DialogInterface; import android.content.Intent; import android.database.Cursor; import android.net.Uri; import android.os.AsyncTask; import android.os.Bundle; import android.os.SystemClock; import android.support.v4.app.FragmentActivity; import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.CursorLoader; import android.support.v4.content.Loader; import android.text.ClipboardManager; import android.text.Editable; import android.text.InputType; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.ContextThemeWrapper; import android.view.KeyEvent; import android.view.Menu; import android.view.MenuItem; import android.view.MotionEvent; import android.view.View; import android.view.View.OnClickListener; import android.view.View.OnFocusChangeListener; import android.view.View.OnLongClickListener; import android.view.View.OnTouchListener; import android.view.ViewGroup; import android.view.animation.AnimationUtils; import android.view.inputmethod.InputMethodManager; import android.webkit.WebView; import android.widget.AdapterView; import android.widget.AdapterView.AdapterContextMenuInfo; import android.widget.Button; import android.widget.EditText; import android.widget.ExpandableListView; import android.widget.ExpandableListView.OnChildClickListener; import android.widget.LinearLayout; import android.widget.ListView; import android.widget.MultiAutoCompleteTextView; import android.widget.SimpleCursorAdapter; import android.widget.SimpleCursorTreeAdapter; import android.widget.TextView; import android.widget.TextView.OnEditorActionListener; import android.widget.Toast; // TODO high: move category strings to a system that can handle runtime i18n changes. maybe put string refs in the DB? // TODO high: fix mdpi app icon on Android 1.6 // TODO high: ldpi smaller icon for about // TODO med: add implicit "last result" eg. "1+1=" then press "+1" to get "3" // TODO med: auto-ranging for metric units (auto add "kilo-" or "micro-") // TODO med: allow for returning composite Imperial and time units. eg. "3 hours + 12 minutes" instead of "3.2 hours" // TODO med: remove soft keyboard for non-touch devices // TODO med: look into performance bug on ADP device. Slows down considerably when backspacing whole entry. // TODO med: add date headers for history, to consolidate items ("yesterday", "1 week ago", etc.) // TODO med: show keyboard icon for 2nd tap (can't do this easily, as one can't detect if soft keyboard is shown or not). May need to scrap this idea. // TODO low: longpress on unit for description (look in unit addition error message for hints) // TODO low: Auto-scale text for display (square) public class Units extends FragmentActivity implements OnClickListener, OnEditorActionListener, OnTouchListener, OnLongClickListener, LoaderCallbacks<Cursor> { @SuppressWarnings("unused") private final static String TAG = Units.class.getSimpleName(); private MultiAutoCompleteTextView wantEditText; private MultiAutoCompleteTextView haveEditText; private TextView resultView; private ListView history; private LinearLayout historyDrawer; private Button historyClose; private WorkspaceView workspace; private UnitUsageDBHelper unitUsageDBHelper; private HistoryAdapter mHistoryAdapter; public final static String XMLNS = "http://staticfree.info/ns/android/units"; public final static String ACTION_USE_UNIT = "info.staticfree.android.units.ACTION_USE_UNIT", EXTRA_UNIT_NAME = "info.staticfree.android.units.EXTRA_UNIT_NAME"; public final static String STATE_RESULT_TEXT = "info.staticfree.android.units.RESULT_TEXT", STATE_DRAWER_OPENED = "info.staticfree.android.units.DRAWER_OPENED", STATE_DIALOG_UNIT_CATEGORY = "info.staticfree.android.units.STATE_DIALOG_UNIT_CATEGORY"; private static final int REQUEST_PICK_UNIT = 0; private static final int LOADER_HISTORY = 100, LOADER_USAGE = 101; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); wantEditText = ((MultiAutoCompleteTextView) findViewById(R.id.want)); haveEditText = ((MultiAutoCompleteTextView) findViewById(R.id.have)); defaultInputType = wantEditText.getInputType(); wantEditText.setOnFocusChangeListener(inputBoxOnFocusChange); haveEditText.setOnFocusChangeListener(inputBoxOnFocusChange); resultView = ((TextView) findViewById(R.id.result)); history = ((ListView) findViewById(R.id.history_list)); historyDrawer = ((LinearLayout) findViewById(R.id.history_drawer)); historyClose = ((Button) findViewById(R.id.history_close)); workspace = (WorkspaceView) findViewById(R.id.numpad_switcher); //workspace.setTouchSlop(); // XXX scale //workspace.setShowTabIndicator(false); mHistoryAdapter = new HistoryAdapter(this, null); history.setAdapter(mHistoryAdapter); // TODO consolidate listeners history.setOnItemClickListener(new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View v, int position, long id) { setCurrentEntry( ContentUris.withAppendedId(HistoryEntry.CONTENT_URI, mHistoryAdapter.getItemId(position))); setHistoryVisible(false); } }); history.setOnCreateContextMenuListener(this); resultView.setOnClickListener(this); resultView.setOnCreateContextMenuListener(this); historyClose.setOnClickListener(this); // Go through the numberpad and add all the onClick listeners. // Make sure to update if the layout changes. setGridChildrenListener(((LinearLayout) findViewById(R.id.numberpad)), buttonListener, buttonListener); setGridChildrenListener((ViewGroup) findViewById(R.id.numberpad2), buttonListener, buttonListener); final View backspace = findViewById(R.id.backspace); backspace.setOnClickListener(buttonListener); backspace.setOnLongClickListener(buttonListener); unitUsageDBHelper = new UnitUsageDBHelper(this); final Object instance = getLastNonConfigurationInstance(); if (instance instanceof LoadInitialUnitUsageTask) { mLoadInitialUnitUsageTask = (LoadInitialUnitUsageTask) instance; mLoadInitialUnitUsageTask.setActivity(this); } else { if (unitUsageDBHelper.getUnitUsageDbCount() == 0) { mLoadInitialUnitUsageTask = new LoadInitialUnitUsageTask(); mLoadInitialUnitUsageTask.setActivity(this); mLoadInitialUnitUsageTask.execute(); } } wantEditText.setOnEditorActionListener(this); final UnitsMultiAutoCompleteTokenizer tokenizer = new UnitsMultiAutoCompleteTokenizer(); haveEditText.setTokenizer(tokenizer); wantEditText.setTokenizer(tokenizer); haveEditText.setOnTouchListener(this); wantEditText.setOnTouchListener(this); if (savedInstanceState != null) { resultView.setText(savedInstanceState.getCharSequence(STATE_RESULT_TEXT)); setHistoryVisible(savedInstanceState.getBoolean(STATE_DRAWER_OPENED, false), false); mDialogUnitCategoryUnit = savedInstanceState.getString(STATE_DIALOG_UNIT_CATEGORY); } final Intent intent = getIntent(); handleIntent(intent); getSupportLoaderManager().initLoader(LOADER_HISTORY, null, this); getSupportLoaderManager().initLoader(LOADER_USAGE, null, this); } private void handleIntent(Intent intent) { final String action = intent.getAction(); if (Intent.ACTION_SEARCH.equals(action)) { final Intent pickUnit = new Intent(Intent.ACTION_PICK, UsageEntry.CONTENT_URI); final String query = intent.getExtras().getString(SearchManager.QUERY); pickUnit.putExtra(UnitList.EXTRA_UNIT_QUERY, query); startActivityForResult(pickUnit, REQUEST_PICK_UNIT); } else if (ACTION_USE_UNIT.equals(action)) { sendUnitAsSoftKeyboard(intent.getData()); } } @Override protected void onNewIntent(Intent intent) { super.onNewIntent(intent); handleIntent(intent); } private UnitUsageDBHelper.UnitCursorAdapter mHaveUsageAdapter; private UnitUsageDBHelper.UnitCursorAdapter mWantUsageAdapter; @Override protected void onStart() { super.onStart(); mHaveUsageAdapter = new UnitUsageDBHelper.UnitCursorAdapter(this, null, wantEditText); haveEditText.setAdapter(mHaveUsageAdapter); mWantUsageAdapter = new UnitUsageDBHelper.UnitCursorAdapter(this, null, haveEditText); wantEditText.setAdapter(mWantUsageAdapter); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putCharSequence(STATE_RESULT_TEXT, resultView.getText()); outState.putBoolean(STATE_DRAWER_OPENED, isHistoryVisible()); outState.putString(STATE_DIALOG_UNIT_CATEGORY, mDialogUnitCategoryUnit); } // @Override // public Object onRetainNonConfigurationInstance() { // return mLoadInitialUnitUsageTask; // } @Override protected void onActivityResult(int requestCode, int resultCode, Intent data) { super.onActivityResult(requestCode, resultCode, data); switch (requestCode) { case REQUEST_PICK_UNIT: { if (resultCode == RESULT_OK) { sendTextAsSoftKeyboard(data.getExtras().getString(EXTRA_UNIT_NAME) + " "); } } break; } } public Loader<Cursor> onCreateLoader(int id, Bundle args) { switch (id) { case LOADER_HISTORY: return new CursorLoader(this, HistoryEntry.CONTENT_URI, HistoryAdapter.PROJECTION, null, null, HistoryEntry.SORT_DEFAULT); case LOADER_USAGE: return new CursorLoader(this, UsageEntry.CONTENT_URI, null, null, null, UnitUsageDBHelper.USAGE_SORT); default: throw new IllegalArgumentException("unknown loader ID"); } } public void onLoadFinished(Loader<Cursor> loader, Cursor c) { switch (loader.getId()) { case LOADER_HISTORY: mHistoryAdapter.swapCursor(c); break; case LOADER_USAGE: mHaveUsageAdapter.swapCursor(c); mWantUsageAdapter.swapCursor(c); } } public void onLoaderReset(Loader<Cursor> loader) { // TODO Auto-generated method stub } /** * @param vg Given a view group with view groups inside it, set all children to have the same onClickListeners. * @param onClickListener * @param onLongClickListener */ private void setGridChildrenListener(ViewGroup vg, OnClickListener onClickListener, OnLongClickListener onLongClickListener) { // Go through the children and add all the onClick listeners. // Make sure to update if the layout changes. final int rows = vg.getChildCount(); for (int row = 0; row < rows; row++) { final ViewGroup v = (ViewGroup) vg.getChildAt(row); final int columns = v.getChildCount(); for (int column = 0; column < columns; column++) { final View button = v.getChildAt(column); button.setOnClickListener(onClickListener); button.setOnLongClickListener(onLongClickListener); } } } private boolean isHistoryVisible() { return historyDrawer.getVisibility() == View.VISIBLE; } private void setHistoryVisible(boolean visible) { setHistoryVisible(visible, true); } private void setHistoryVisible(boolean visible, boolean animate) { if (visible && historyDrawer.getVisibility() == View.INVISIBLE) { if (animate) { historyDrawer .startAnimation(AnimationUtils.loadAnimation(getApplicationContext(), R.anim.history_show)); } historyDrawer.setVisibility(View.VISIBLE); historyDrawer.requestFocus(); } else if (!visible && historyDrawer.getVisibility() == View.VISIBLE) { if (animate) { historyDrawer .startAnimation(AnimationUtils.loadAnimation(getApplicationContext(), R.anim.history_hide)); } historyDrawer.setVisibility(View.INVISIBLE); } } /** * Adds the given entry to the history database. * * @author steve * */ private class AddToHistoryRunnable implements Runnable { private final String haveExpr, wantExpr; private final Double result; public AddToHistoryRunnable(String haveExpr, String wantExpr, Double result) { this.haveExpr = haveExpr; this.wantExpr = wantExpr; this.result = result; } public void run() { final ContentValues cv = new ContentValues(); cv.put(HistoryEntry._HAVE, haveExpr); cv.put(HistoryEntry._WANT, wantExpr); cv.put(HistoryEntry._RESULT, result); getContentResolver().insert(HistoryEntry.CONTENT_URI, cv); } } // TODO make reciprocal notice better animated so it doesn't modify main layout public void addToHistory(String haveExpr, String wantExpr, Double result, boolean reciprocal) { haveExpr = haveExpr.trim(); wantExpr = wantExpr.trim(); new AddToUsageTask().execute(haveExpr, wantExpr); haveExpr = reciprocal ? "1(" + haveExpr + ")" : haveExpr; resultView.setText(HistoryEntry.toCharSequence(haveExpr, wantExpr, result)); // done on a new thread to avoid hanging the UI while the DB is being updated. new Thread(new AddToHistoryRunnable(haveExpr, wantExpr, result)).start(); final View reciprocalNotice = findViewById(R.id.reciprocal_notice); if (reciprocal) { resultView.requestFocus(); reciprocalNotice.setVisibility(View.VISIBLE); reciprocalNotice.startAnimation(AnimationUtils.makeInAnimation(this, true)); } else { reciprocalNotice.setVisibility(View.GONE); resultView.setError(null); } } private final static String[] PROJECTION_LOAD_FROM_HISTORY = { HistoryEntry._HAVE, HistoryEntry._WANT, HistoryEntry._RESULT }; /** * Set the current entries to that of a history entry. * * @param entry a history entry URI */ private void setCurrentEntry(Uri entry) { final Cursor c = getContentResolver().query(entry, PROJECTION_LOAD_FROM_HISTORY, null, null, null); if (c.moveToFirst()) { setCurrentEntry(c.getString(c.getColumnIndex(HistoryEntry._HAVE)), c.getString(c.getColumnIndex(HistoryEntry._WANT))); } c.close(); } private void setCurrentEntry(String have, String want) { haveEditText.setText(have + " ");// extra space is to prevent auto-complete from triggering. wantEditText.setText(want + (want.length() > 0 ? " " : "")); haveEditText.requestFocus(); haveEditText.setSelection(haveEditText.length()); } /** * Converts the history entry to a CharSequence. * * @param entry A history entry URI * @return the name of the unit, as a CharSequence */ private CharSequence getEntryAsCharSequence(Uri entry) { CharSequence text = null; final Cursor c = getContentResolver().query(entry, PROJECTION_LOAD_FROM_HISTORY, null, null, null); if (c.moveToFirst()) { text = HistoryEntry.toCharSequence(c, c.getColumnIndex(HistoryEntry._HAVE), c.getColumnIndex(HistoryEntry._WANT), c.getColumnIndex(HistoryEntry._RESULT)); } c.close(); return text; } public static HashMap<Character, String> UNICODE_TRANS = new HashMap<Character, String>(); static { UNICODE_TRANS.put('', "/"); UNICODE_TRANS.put('', "*"); UNICODE_TRANS.put('', "/"); UNICODE_TRANS.put('', "*"); UNICODE_TRANS.put('', "^2"); UNICODE_TRANS.put('', "^3"); UNICODE_TRANS.put('?', "^4"); UNICODE_TRANS.put('', "-"); UNICODE_TRANS.put('', "micro"); UNICODE_TRANS.put('', "pi"); UNICODE_TRANS.put('', "pi"); UNICODE_TRANS.put('', "euro"); UNICODE_TRANS.put('', "japanyen"); UNICODE_TRANS.put('', "greatbritainpound"); UNICODE_TRANS.put('', "sqrt"); UNICODE_TRANS.put('', "cuberoot"); UNICODE_TRANS.put('', "1|2"); UNICODE_TRANS.put('', "1|3"); UNICODE_TRANS.put('', "2|3"); UNICODE_TRANS.put('', "1|4"); UNICODE_TRANS.put('', "1|5"); UNICODE_TRANS.put('', "2|5"); UNICODE_TRANS.put('', "3|5"); UNICODE_TRANS.put('', "1|6"); UNICODE_TRANS.put('', "1|8"); UNICODE_TRANS.put('', "3|8"); UNICODE_TRANS.put('?', "5|8"); } public static String unicodeToAscii(String unicodeInput) { final StringBuilder sb = new StringBuilder(); final int len = unicodeInput.length(); for (int i = 0; i < len; i++) { final char c = unicodeInput.charAt(i); final String sub = UNICODE_TRANS.get(c); if (sub != null) { sb.append(sub); } else { sb.append(c); } } return sb.toString(); } // TODO filter error messages and output translate to unicode from engine. error msgs and Inifinity public void go() { String haveStr = haveEditText.getText().toString().trim(); String wantStr = wantEditText.getText().toString().trim(); try { Value have = null; try { if (haveStr.length() == 0) { haveEditText.requestFocus(); haveEditText.setError(getText(R.string.err_have_empty)); return; } haveStr = ValueGui.closeParens(haveStr); have = ValueGui.fromUnicodeString(haveStr); } catch (final EvalError e) { haveEditText.requestFocus(); haveEditText.setError(e.getLocalizedMessage()); return; } Value want = null; Function func = null; try { func = DefinedFunction.table.get(wantStr); if (func == null && wantStr.endsWith("(")) { func = DefinedFunction.table.get(wantStr.subSequence(0, wantStr.length() - 1)); } if (func == null) { wantStr = ValueGui.closeParens(wantStr); want = ValueGui.fromUnicodeString(wantStr); } } catch (final EvalError e) { wantEditText.requestFocus(); wantEditText.setError(e.getLocalizedMessage()); return; } Double resultVal; boolean reciprocal = false; try { wantEditText.setError(null); // if no want value is specified, provide a definition. if (wantStr.length() > 0) { if (func != null) { // functions are a special case and don't have a reciprocal, so the result // is just stored in the wantStr. resultVal = null; wantStr = ValueGui.convertNonInteractive(ValueGui.fromUnicodeString(haveStr), func); } else { resultVal = ValueGui.convertNonInteractive(have, want); } } else { resultVal = have.factor; final StringBuffer haveDef = new StringBuffer(); haveDef.append(have.numerator.asString()); if (have.denominator.size() > 0) { haveDef.append(" ").append(have.denominator.asString()); } wantStr = haveDef.toString(); } } catch (final ReciprocalException re) { reciprocal = true; resultVal = ValueGui.convertNonInteractive(re.reciprocal, want); } allClear(); addToHistory(haveStr, wantStr, resultVal, reciprocal); } catch (final ConversionException e) { resultView.setText(null); wantEditText.requestFocus(); wantEditText.setError(getText(R.string.err_no_conform)); return; } } public void allClear() { haveEditText.getEditableText().clear(); haveEditText.setError(null); wantEditText.getEditableText().clear(); wantEditText.setError(null); haveEditText.requestFocus(); } public void onClick(View v) { switch (v.getId()) { case R.id.result: setHistoryVisible(true); break; case R.id.history_close: setHistoryVisible(false); break; } } public boolean onLongClick(View v) { switch (v.getId()) { case R.id.result: v.showContextMenu(); return true; } return false; } private static final int MENU_COPY = 0, MENU_SEND = 1, MENU_USE_RESULT = 2, MENU_REEDIT = 3; @Override public void onCreateContextMenu(ContextMenu menu, View v, ContextMenuInfo menuInfo) { menu.add(Menu.NONE, MENU_REEDIT, Menu.FIRST, R.string.ctx_menu_reedit); menu.add(Menu.NONE, MENU_COPY, Menu.CATEGORY_SYSTEM, android.R.string.copy); menu.add(Menu.NONE, MENU_SEND, Menu.CATEGORY_SYSTEM, R.string.ctx_menu_send); menu.add(Menu.NONE, MENU_USE_RESULT, Menu.CATEGORY_SECONDARY, R.string.ctx_menu_use_result); } @Override public boolean onContextItemSelected(MenuItem item) { final ContextMenuInfo ctxMenuInfo = item.getMenuInfo(); int position = history.getCount() - 1; if (ctxMenuInfo instanceof AdapterContextMenuInfo) { position = ((AdapterContextMenuInfo) ctxMenuInfo).position; } final Uri itemUri = ContentUris.withAppendedId(HistoryEntry.CONTENT_URI, mHistoryAdapter.getItemId(position)); switch (item.getItemId()) { case MENU_COPY: { final CharSequence itemText = getEntryAsCharSequence(itemUri); final ClipboardManager clipboard = (ClipboardManager) getSystemService(CLIPBOARD_SERVICE); clipboard.setText(itemText); Toast.makeText(this, getString(R.string.toast_copy, itemText), Toast.LENGTH_SHORT).show(); } break; case MENU_REEDIT: { setCurrentEntry(itemUri); setHistoryVisible(false); } break; case MENU_SEND: { final CharSequence itemText = getEntryAsCharSequence(itemUri); startActivity(Intent.createChooser( new Intent(Intent.ACTION_SEND).setType("text/plain").putExtra(Intent.EXTRA_TEXT, itemText), getText(R.string.ctx_menu_send_title))); } break; case MENU_USE_RESULT: { final Cursor c = getContentResolver().query(itemUri, PROJECTION_LOAD_FROM_HISTORY, null, null, null); if (c.moveToFirst()) { final int resultCol = c.getColumnIndex(HistoryEntry._RESULT); setCurrentEntry((c.isNull(resultCol) ? "" : (c.getDouble(resultCol) + " ")) + c.getString(c.getColumnIndex(HistoryEntry._WANT)), ""); setHistoryVisible(false); } c.close(); } break; } return super.onContextItemSelected(item); } @Override public boolean onCreateOptionsMenu(Menu menu) { super.onCreateOptionsMenu(menu); getMenuInflater().inflate(R.menu.main, menu); return true; } @Override public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.swap_inputs: { if (getCurrentFocus() == haveEditText) { swapInputs(haveEditText, wantEditText); } else if (getCurrentFocus() == wantEditText) { swapInputs(wantEditText, haveEditText); } else { swapInputs(null, null); } return true; } case R.id.about: showDialog(DIALOG_ABOUT); return true; case R.id.show_history: setHistoryVisible(true); return true; case R.id.clear_history: getContentResolver().delete(HistoryEntry.CONTENT_URI, null, null); resultView.setText(null); return true; case R.id.search: onSearchRequested(); return true; default: return super.onOptionsItemSelected(item); } } /** * Read an InputStream into a String until it hits EOF. * * @param in * @return the complete contents of the InputStream * @throws IOException */ static public String inputStreamToString(InputStream in) throws IOException { final int bufsize = 8196; final char[] cbuf = new char[bufsize]; final StringBuffer buf = new StringBuffer(bufsize); final InputStreamReader in_reader = new InputStreamReader(in); for (int readBytes = in_reader.read(cbuf, 0, bufsize); readBytes > 0; readBytes = in_reader.read(cbuf, 0, bufsize)) { buf.append(cbuf, 0, readBytes); } return buf.toString(); } private void sendTextAsSoftKeyboard(String text) { dispatchKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), text, Units.class.hashCode(), KeyEvent.FLAG_SOFT_KEYBOARD)); } private void sendTextAsSoftKeyboard(String text, boolean moveToDefault) { dispatchKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), text, Units.class.hashCode(), KeyEvent.FLAG_SOFT_KEYBOARD)); if (moveToDefault) { workspace.moveToDefaultScreen(); } } private void sendUnitAsSoftKeyboard(Uri unit) { final String[] projection = { UsageEntry._ID, UsageEntry._UNIT }; final Cursor c = getContentResolver().query(unit, projection, null, null, null); if (c.moveToFirst()) { sendTextAsSoftKeyboard(c.getString(c.getColumnIndex(UsageEntry._UNIT)) + " "); } c.close(); } private final OnChildClickListener allUnitChildClickListener = new OnChildClickListener() { public boolean onChildClick(ExpandableListView parent, View v, int groupPosition, int childPosition, long id) { sendTextAsSoftKeyboard(((TextView) v).getText().toString() + " "); dismissDialog(DIALOG_ALL_UNITS); return true; } }; private final DialogInterface.OnClickListener dialogUnitCategoryOnClickListener = new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { sendUnitAsSoftKeyboard( ContentUris.withAppendedId(UsageEntry.CONTENT_URI, dialogUnitCategoryList.getItemId(which))); workspace.moveToDefaultScreen(); } }; private static final int DIALOG_ABOUT = 0, DIALOG_ALL_UNITS = 1, DIALOG_LOADING_UNITS = 2, DIALOG_UNIT_CATEGORY = 3; @Override protected Dialog onCreateDialog(int id) { switch (id) { case DIALOG_ABOUT: { final Builder builder = new AlertDialog.Builder(this); builder.setTitle(R.string.dialog_about_title); builder.setIcon(R.drawable.icon); try { final WebView wv = new WebView(this); final InputStream is = getAssets().open("README.xhtml"); wv.loadDataWithBaseURL("file:///android_asset/", inputStreamToString(is), "application/xhtml+xml", "utf-8", null); wv.setBackgroundColor(0); builder.setView(wv); } catch (final IOException e) { builder.setMessage(R.string.err_no_load_about); e.printStackTrace(); } builder.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int which) { setResult(RESULT_OK); } }); return builder.create(); } case DIALOG_ALL_UNITS: { final Builder b = new Builder(Units.this); b.setTitle(R.string.dialog_all_units_title); final ExpandableListView unitExpandList = new ExpandableListView(Units.this); unitExpandList.setId(android.R.id.list); final String[] groupProjection = { UsageEntry._ID, UsageEntry._UNIT, UsageEntry._FACTOR_FPRINT }; // any selection below will select from the grouping description final Cursor cursor = managedQuery(UsageEntry.CONTENT_URI_CONFORM_TOP, groupProjection, null, null, UnitUsageDBHelper.USAGE_SORT); unitExpandList.setAdapter(new UnitsExpandableListAdapter(cursor, this, android.R.layout.simple_expandable_list_item_1, android.R.layout.simple_expandable_list_item_1, new String[] { UsageEntry._UNIT }, new int[] { android.R.id.text1 }, new String[] { UsageEntry._UNIT }, new int[] { android.R.id.text1 })); unitExpandList.setCacheColorHint(0); unitExpandList.setOnChildClickListener(allUnitChildClickListener); b.setView(unitExpandList); return b.create(); } case DIALOG_UNIT_CATEGORY: { final Builder b = new Builder(new ContextThemeWrapper(this, android.R.style.Theme_Black)); final String[] from = { UsageEntry._UNIT }; final int[] to = { android.R.id.text1 }; b.setTitle("all units"); final String[] projection = { UsageEntry._ID, UsageEntry._UNIT, UsageEntry._FACTOR_FPRINT }; final Cursor c = managedQuery(UsageEntry.CONTENT_URI, projection, null, null, UnitUsageDBHelper.USAGE_SORT); dialogUnitCategoryList = new SimpleCursorAdapter(this, android.R.layout.select_dialog_item, c, from, to); b.setAdapter(dialogUnitCategoryList, dialogUnitCategoryOnClickListener); return b.create(); } case DIALOG_LOADING_UNITS: { final ProgressDialog pd = new ProgressDialog(this); pd.setIndeterminate(true); pd.setTitle(R.string.app_name); pd.setMessage(getText(R.string.dialog_loading_units)); return pd; } default: throw new IllegalArgumentException("Unknown dialog ID:" + id); } } private String mDialogUnitCategoryUnit = "m"; private SimpleCursorAdapter dialogUnitCategoryList; @Override protected void onPrepareDialog(int id, Dialog dialog) { switch (id) { case DIALOG_UNIT_CATEGORY: { dialog.setTitle(mDialogUnitCategoryUnit); final String[] projection = { UsageEntry._ID, UsageEntry._UNIT, UsageEntry._FACTOR_FPRINT }; final Cursor c = managedQuery( UsageEntry.getEntriesMatchingFprint(UnitUsageDBHelper.getFingerprint(mDialogUnitCategoryUnit)), projection, null, null, UnitUsageDBHelper.USAGE_SORT); dialogUnitCategoryList.changeCursor(c); final ListView lv = ((AlertDialog) dialog).getListView(); lv.setSelectionFromTop(0, 0); } break; default: super.onPrepareDialog(id, dialog); } } private void swapInputs(EditText focused, EditText unfocused) { final Editable e = wantEditText.getText(); int start = 0, end = 0; if (focused != null) { start = focused.getSelectionStart(); end = focused.getSelectionEnd(); } wantEditText.setText(haveEditText.getText()); haveEditText.setText(e); if (unfocused != null) { unfocused.requestFocus(); unfocused.setSelection(start, end); } } public class UnitsExpandableListAdapter extends SimpleCursorTreeAdapter { private final int factorFprintColumn; private final String[] childProjection = { UsageEntry._ID, UsageEntry._UNIT }; public UnitsExpandableListAdapter(Cursor cursor, Activity context, int groupLayout, int childLayout, String[] groupFrom, int[] groupTo, String[] childrenFrom, int[] childrenTo) { super(context, cursor, groupLayout, groupFrom, groupTo, childLayout, childrenFrom, childrenTo); factorFprintColumn = cursor.getColumnIndex(UsageEntry._FACTOR_FPRINT); } @Override protected Cursor getChildrenCursor(Cursor groupCursor) { // Given the group, we return a cursor for all the children within that group. final String factorFprint = groupCursor.getString(factorFprintColumn); final String[] selectionArgs = { factorFprint }; return managedQuery(UsageEntry.CONTENT_URI, childProjection, UsageEntry._FACTOR_FPRINT + "=?", selectionArgs, UnitUsageDBHelper.USAGE_SORT); } } private final ButtonEventListener buttonListener = new ButtonEventListener(); private class ButtonEventListener implements OnClickListener, OnLongClickListener { public void onClick(View v) { switch (v.getId()) { case R.id.backspace: { dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_DEL)); dispatchKeyEvent(new KeyEvent(KeyEvent.ACTION_UP, KeyEvent.KEYCODE_DEL)); } break; case R.id.equal: go(); break; case R.id.unit_entry: { final View currentFocus = getCurrentFocus(); if (currentFocus instanceof MultiAutoCompleteTextView) { // 2000 is just a magic number that is less than the total number of units, // but greater than the number of possibly conforming units. Discovered empirically. if (((MultiAutoCompleteTextView) currentFocus).getAdapter().getCount() > 2000) { onSearchRequested(); } else { ((MultiAutoCompleteTextView) currentFocus).setError(null); ((MultiAutoCompleteTextView) currentFocus).showDropDown(); } } } break; case R.id.length: mDialogUnitCategoryUnit = "m"; showDialog(DIALOG_UNIT_CATEGORY); break; case R.id.weight: mDialogUnitCategoryUnit = "g"; showDialog(DIALOG_UNIT_CATEGORY); break; case R.id.time: mDialogUnitCategoryUnit = "hr"; showDialog(DIALOG_UNIT_CATEGORY); break; // functions case R.id.sin: case R.id.cos: case R.id.tan: case R.id.atan: case R.id.log: case R.id.ln: sendTextAsSoftKeyboard(((Button) v).getText().toString() + "( ", true); break; // constants case R.id.pi: case R.id.light: case R.id.energy: sendTextAsSoftKeyboard(((Button) v).getText().toString() + " ", true); break; case R.id.square: sendTextAsSoftKeyboard(" ", true); break; case R.id.cube: sendTextAsSoftKeyboard(" ", true); break; case R.id.milli: case R.id.kilo: case R.id.centi: { String prefix = ((Button) v).getText().toString(); prefix = prefix.substring(0, prefix.length() - 1); sendTextAsSoftKeyboard(prefix, false); } break; default: sendTextAsSoftKeyboard(((Button) v).getText().toString(), true); } } public boolean onLongClick(View v) { switch (v.getId()) { case R.id.unit_entry: { showDialog(DIALOG_ALL_UNITS); return true; } case R.id.power: { sendTextAsSoftKeyboard("E"); return true; } case R.id.div: { sendTextAsSoftKeyboard("|"); return true; } case R.id.backspace: { final View currentFocus = getCurrentFocus(); if (currentFocus instanceof EditText) { ((EditText) currentFocus).getEditableText().clear(); ((EditText) currentFocus).setError(null); } return true; } } return false; } } public boolean onEditorAction(TextView v, int actionId, KeyEvent event) { switch (v.getId()) { case R.id.want: go(); final InputMethodManager imm = (InputMethodManager) getSystemService(INPUT_METHOD_SERVICE); imm.hideSoftInputFromWindow(v.getWindowToken(), 0); return true; } return false; }; private int defaultInputType; // make sure to reset the input type when losing focus. private final OnFocusChangeListener inputBoxOnFocusChange = new OnFocusChangeListener() { public void onFocusChange(View v, boolean hasFocus) { if (!hasFocus) { ((MultiAutoCompleteTextView) v).setInputType(defaultInputType); } } }; public boolean onTouch(View v, MotionEvent event) { switch (v.getId()) { // this is used to prevent the first touch on these editors from triggering the IME soft keyboard. case R.id.want: case R.id.have: if (event.getAction() == MotionEvent.ACTION_DOWN) { final EditText editor = (EditText) v; if (v.hasFocus()) { editor.setInputType(defaultInputType); v.requestFocus(); return false; } editor.setInputType(InputType.TYPE_NULL); } } return false; } /** * Add the units in the given expression(s) to the usage database. * @author steve * */ private class AddToUsageTask extends AsyncTask<String, Void, Void> { @Override protected Void doInBackground(String... params) { for (final String param : params) { UnitUsageDBHelper.logUnitsInExpression(param, getContentResolver()); } return null; } } private LoadInitialUnitUsageTask mLoadInitialUnitUsageTask; /** * Load the initial usage data on the first run of the application. * * @author steve * */ private class LoadInitialUnitUsageTask extends AsyncTask<Void, Void, Void> { private Activity mActivity; public void setActivity(Activity activity) { mActivity = activity; } @Override protected void onPreExecute() { showDialog(DIALOG_LOADING_UNITS); } @Override protected Void doInBackground(Void... params) { unitUsageDBHelper.loadInitialUnitUsage(); unitUsageDBHelper.loadUnitClassifications(); return null; } @Override protected void onPostExecute(Void result) { try { if (mActivity != null) { mActivity.dismissDialog(DIALOG_LOADING_UNITS); } } catch (final IllegalArgumentException ie) { // it's alright if it was dismissed already. } mLoadInitialUnitUsageTask = null; } } }