package org.openintents.shopping.ui.widget; import android.app.Activity; import android.content.ContentResolver; import android.content.ContentValues; import android.content.Context; import android.database.ContentObserver; import android.database.Cursor; import android.database.DatabaseUtils; import android.graphics.Typeface; import android.net.Uri; import android.os.Handler; import android.text.Editable; import android.text.TextWatcher; import android.util.AttributeSet; import android.util.Log; import android.view.ContextMenu; import android.view.ContextMenu.ContextMenuInfo; import android.view.View; import android.view.ViewGroup; import android.widget.*; import android.widget.SimpleCursorAdapter.ViewBinder; import org.openintents.shopping.R; import org.openintents.shopping.library.provider.ShoppingContract; import org.openintents.shopping.library.provider.ShoppingContract.ItemStores; import org.openintents.shopping.library.provider.ShoppingContract.Stores; import org.openintents.shopping.library.util.PriceConverter; import org.openintents.shopping.library.util.ShoppingUtils; import org.openintents.shopping.ui.PreferenceActivity; /** * View to show a list of stores for a specific item */ public class StoreListView extends ListView { private final static String TAG = "StoreListView"; private final static boolean debug = false; private Typeface mCurrentTypeface; public int mPriceVisibility; public String mTextTypeface; public float mTextSize; public boolean mTextUpperCaseFont; public int mTextColor; public int mTextColorPrice; public int mTextColorChecked; public boolean mShowCheckBox; public boolean mInTextInput; public boolean mBinding; private boolean mTextChanged; private final String[] mStringItems = new String[]{ "itemstores." + ItemStores._ID, Stores.NAME, ItemStores.STOCKS_ITEM, ItemStores.PRICE, ItemStores.AISLE, "stores._id as store_id"}; private final static int cursorColumnID = 0; private final static int cursorColumnNAME = 1; private final static int cursorColumnSTOCKS_ITEM = 2; private final static int cursorColumnPRICE = 3; private final static int cursorColumnAISLE = 4; private final static int cursorColumnSTORE_ID = 5; private Cursor mCursorItemstores; private long mItemId; private long mListId; private ContentValues[] mBackup; private boolean mDirty; private EditText m_lastView; private int m_lastCol; public void applyUpdate() { if (m_lastView == null) { return; } String val = m_lastView.getText().toString(); if (m_lastCol == cursorColumnPRICE) { val = Long.toString(PriceConverter.getCentPriceFromString(val)); } Integer row = (Integer) m_lastView.getTag(); if (row != null) { if (debug) { Log.d(TAG, "Text changed to " + val + " @ pos " + row + ", col " + m_lastCol); } maybeUpdate(row, m_lastCol, val); } m_lastView = null; } /** * Extend the SimpleCursorAdapter to handle updates to the data */ public class mSimpleCursorAdapter extends SimpleCursorAdapter implements ViewBinder { private class EditTextWatcher implements TextWatcher, OnFocusChangeListener { private int mCol; private EditText mView; public EditTextWatcher(EditText v, int col) { if (debug) { Log.d(TAG, "New EditTextWatcher for " + v.toString() + " col " + col); } mView = v; mCol = col; } @Override public void afterTextChanged(Editable s) { if (mBinding) { return; // for update purposes, doesn't count as change } if (mView != m_lastView) { mView.setOnFocusChangeListener(this); // applyUpdate(); } m_lastView = mView; m_lastCol = mCol; } @Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } @Override public void onTextChanged(CharSequence s, int start, int before, int count) { } @Override public void onFocusChange(View v, boolean hasFocus) { if (v == m_lastView && hasFocus == false) { mInTextInput = true; applyUpdate(); mInTextInput = false; } } } /** * Constructor simply calls super class. * * @param context Context. * @param layout Layout. * @param c Cursor. * @param from Projection from. * @param to Projection to. */ mSimpleCursorAdapter(final Context context, final int layout, final Cursor c, final String[] from, final int[] to) { super(context, layout, c, from, to); super.setViewBinder(this); } @Override public View newView(Context context, Cursor cursor, ViewGroup parent) { View view = super.newView(context, cursor, parent); EditText v; v = (EditText) view.findViewById(R.id.price); v.addTextChangedListener(new EditTextWatcher(v, cursorColumnPRICE)); v.setVisibility(mPriceVisibility); v = (EditText) view.findViewById(R.id.aisle); v.addTextChangedListener(new EditTextWatcher(v, cursorColumnAISLE)); v.setVisibility(mPriceVisibility); return view; } /** * Additionally to the standard bindView, we also check for STATUS, and * strike the item through if BOUGHT. */ @Override public void bindView(final View view, final Context context, final Cursor cursor) { // set tags to null during binding, to help avoid extra db updates // while binding EditText v; v = (EditText) view.findViewById(R.id.price); v.setTag(null); v = (EditText) view.findViewById(R.id.aisle); v.setTag(null); mBinding = true; super.bindView(view, context, cursor); mBinding = false; boolean status = cursor.getInt(cursorColumnSTOCKS_ITEM) != 0; final int cursorpos = cursor.getPosition(); CheckBox c = (CheckBox) view.findViewById(R.id.check); if (debug) { Log.i(TAG, "bindview: pos = " + cursor.getPosition()); } // set style for check box c.setTag(cursor.getPosition()); c.setVisibility(CheckBox.VISIBLE); c.setChecked(status); // The parent view knows how to deal with clicks. // We just pass the click through. // c.setClickable(false); c.setOnClickListener(new OnClickListener() { @Override public void onClick(View v) { if (debug) { Log.d(TAG, "Click: "); } toggleItemstore(cursorpos); } }); TextView t; t = (TextView) view.findViewById(R.id.name); t.setOnCreateContextMenuListener(new View.OnCreateContextMenuListener() { public void onCreateContextMenu(ContextMenu contextmenu, View view, ContextMenuInfo info) { // Context menus are created in the main activity // ItemStoresActivity } }); v = (EditText) view.findViewById(R.id.price); v.setTag(cursor.getPosition()); v = (EditText) view.findViewById(R.id.aisle); v.setTag(cursor.getPosition()); } public boolean setViewValue(View view, Cursor cursor, int i) { int id = view.getId(); if (id == R.id.price) { long price = cursor.getLong(cursorColumnPRICE); if (price != 0) { String text = PriceConverter.getStringFromCentPrice(price); ((TextView) view).setText(text); return true; } } // let SimpleCursorAdapter handle the binding. return false; } @Override public void setViewBinder(ViewBinder viewBinder) { throw new RuntimeException("this adapter implements setViewValue"); } } private ContentObserver mContentObserver = new ContentObserver(new Handler()) { @Override public void onChange(boolean selfChange) { super.onChange(selfChange); mDirty = true; if (mCursorItemstores != null && !mInTextInput) { try { requery(); } catch (IllegalStateException e) { Log.e(TAG, "IllegalStateException ", e); // Somehow the logic is not completely right yet... mCursorItemstores = null; } } } }; public StoreListView(Context context, AttributeSet attrs, int defStyle) { super(context, attrs, defStyle); init(); } public StoreListView(Context context, AttributeSet attrs) { super(context, attrs); init(); } public StoreListView(Context context) { super(context); init(); } private void init() { } public void onResume() { // Content observer registered at fillItems() // registerContentObserver(); } public void onPause() { unregisterContentObserver(); } private void backupValues() { int nRows = mCursorItemstores.getCount(); if (mBackup != null) { return; } mBackup = new ContentValues[nRows]; int i = 0; while (mCursorItemstores.moveToNext()) { mBackup[i] = new ContentValues(); DatabaseUtils.cursorRowToContentValues(mCursorItemstores, mBackup[i++]); } } public void undoChanges() { for (int i = 0; i < mBackup.length; i++) { ContentValues cv = mBackup[i]; String storeId = cv.getAsString("store_id"); if (cv.getAsString("_id") == null) { // dummy record. delete any itemstores for this item id and // store id. // (may have been created during editing) ContentResolver cr = getContext().getContentResolver(); Cursor existingItems = cr.query(ItemStores.CONTENT_URI, new String[]{ItemStores._ID}, "store_id = ? AND item_id = ?", new String[]{storeId, String.valueOf(mItemId)}, null ); if (existingItems.getCount() > 0) { existingItems.moveToFirst(); long id = existingItems.getLong(cursorColumnID); cr.delete( ItemStores.CONTENT_URI.buildUpon() .appendPath(String.valueOf(id)).build(), null, null ); existingItems.close(); } } else { // real record, restore its values. // long itemstore_id = cv.getAsLong("_id"); boolean has_item = cv.getAsBoolean("stocks_item"); String price = cv.getAsString("price"); String aisle = cv.getAsString("aisle"); ShoppingUtils.addItemToStore(getContext(), mItemId, Long.parseLong(storeId), has_item, aisle, price, false); } } } /** * @param activity Activity to manage the cursor. * @param listId * @return */ public Cursor fillItems(Activity activity, long listId, long itemId) { mListId = listId; mItemId = itemId; String sortOrder = "stores.name"; if (mCursorItemstores != null && !mCursorItemstores.isClosed()) { mCursorItemstores.close(); } // Get a cursor for all stores mCursorItemstores = getContext().getContentResolver().query( ItemStores.CONTENT_URI.buildUpon().appendPath("item") .appendPath(String.valueOf(mItemId)) .appendPath(String.valueOf(mListId)).build(), mStringItems, null, null, sortOrder ); activity.startManagingCursor(mCursorItemstores); registerContentObserver(); if (mCursorItemstores == null) { Log.e(TAG, "missing shopping provider"); setAdapter(new ArrayAdapter<String>(this.getContext(), android.R.layout.simple_list_item_1, new String[]{"no shopping provider"})); return mCursorItemstores; } backupValues(); int layout_row = R.layout.list_item_store; mPriceVisibility = PreferenceActivity .getUsingPerStorePricesFromPrefs(getContext()) ? View.VISIBLE : View.INVISIBLE; mSimpleCursorAdapter adapter = new mSimpleCursorAdapter( this.getContext(), // Use a template that displays a text view layout_row, // Give the cursor to the list adapter mCursorItemstores, // Map the IMAGE and NAME to... new String[]{Stores.NAME, ItemStores.PRICE, ItemStores.AISLE}, // the view defined in the XML template new int[]{R.id.name, R.id.price, R.id.aisle}); setAdapter(adapter); return mCursorItemstores; } /** * */ private void registerContentObserver() { getContext().getContentResolver() .registerContentObserver( ShoppingContract.ItemStores.CONTENT_URI, true, mContentObserver); } private void unregisterContentObserver() { getContext().getContentResolver().unregisterContentObserver( mContentObserver); } public void toggleItemstore(int position) { if (mCursorItemstores.getCount() <= position) { Log.e(TAG, "toggle inexistent item. Probably clicked too quickly?"); return; } mCursorItemstores.moveToPosition(position); long oldstatus = 0; // should first check if the itemstore record exists... String itemstore_id; if (mCursorItemstores.isNull(0)) { long storeId = mCursorItemstores.getLong(cursorColumnSTORE_ID); long isid = ShoppingUtils.addItemToStore(getContext(), mItemId, storeId, "", "", false); itemstore_id = Long.toString(isid); } else { itemstore_id = mCursorItemstores.getString(cursorColumnID); oldstatus = mCursorItemstores.getLong(cursorColumnSTOCKS_ITEM); } // Toggle status: long newstatus = 1 - oldstatus; ContentValues values = new ContentValues(); values.put(ItemStores.STOCKS_ITEM, newstatus); if (debug) { Log.d(TAG, "update row " + itemstore_id + ", newstatus " + newstatus); } getContext().getContentResolver().update( Uri.withAppendedPath(ShoppingContract.ItemStores.CONTENT_URI, itemstore_id), values, null, null ); requery(); invalidate(); } public void maybeUpdate(int position, int column, String new_val) { if (mCursorItemstores.getCount() <= position) { Log.e(TAG, "edit nonexistent item."); return; } mCursorItemstores.moveToPosition(position); String old_val = mCursorItemstores.getString(column); if (new_val.equals(old_val)) { return; } if (mCursorItemstores.isNull(0)) { long storeId = mCursorItemstores.getLong(cursorColumnSTORE_ID); String aisle = ""; String price = ""; if (column == 3) { price = new_val; } if (column == 4) { aisle = new_val; } ShoppingUtils.addItemToStore(getContext(), mItemId, storeId, aisle, price, false); /* * At the corresponding points in the item view, we would requery * and invalidate. However that is mainly because the editing * happens in widgets outside the list view itself, where here it * happens in EditTexts directly in the list. So we probably don't * need to invalidate() here. Do we really need to requery()? * Probably somewhere, perhaps not here. */ // requery(); // invalidate(); // need to do those somewhere else. mDirty = true; return; } String itemstore_id = mCursorItemstores.getString(cursorColumnID); Uri uri = Uri.withAppendedPath(ItemStores.CONTENT_URI, itemstore_id); ContentValues cv = new ContentValues(); cv.put(mStringItems[column], new_val); getContext().getContentResolver().update(uri, cv, null, null); // see comment above // requery(); // invalidate(); mDirty = true; } public void requery() { if (debug) { Log.d(TAG, "requery()"); } mCursorItemstores.requery(); mDirty = false; } public String getStoreName(int cursorPosition) { String name = ""; Cursor c = mCursorItemstores; if (c != null) { if (c.moveToPosition(cursorPosition)) { name = c.getString(cursorColumnNAME); } } return name; } public String getStoreId(int cursorPosition) { String id = null; Cursor c = mCursorItemstores; if (c != null) { if (c.moveToPosition(cursorPosition)) { id = c.getString(cursorColumnSTORE_ID); } } return id; } }