DbTradeAlert for Android: Add Security and Watchlist Management – Part 2

First post in this series: Introduction to DbTradeAlert

Previous post: DbTradeAlert for Android: Add Security and Watchlist Management – Part 1


1.4 Add Save Functionality for New Watchlists

“Add Save Functionality” simply means get WatchlistEditActivity’s OK button working:

public class WatchlistEditActivity extends AppCompatActivity {
    // ...

    public void onOkButtonClick(View view) {
        // Get name
        String name = "";
        EditText editText = (EditText) findViewById(R.id.nameEditText);
        if (editText.length() > 0) {
            name = editText.getText().toString();
        }
        // Get securities to include in watchlist
        ListView listView = (ListView) findViewById(R.id.securitiesListView);
        long[] securityIds = listView.getCheckedItemIds();
        // Save edits
        this.dbHelper.updateOrCreateWatchlist(name, securityIds, this.watchlistId);
        setResult(RESULT_OK, getIntent());
        finish();
    } // onOkButtonClick()

    // ...
}

The method extracts the (new) watchlist’s name and the securities to include in it from the controls and passes it to DbHelper.updateOrCreateWatchlist(). After that it sets the activity’s result to RESULT_OK and closes the screen.

public class DbHelper extends SQLiteOpenHelper {
    private final static String DELETE_RESULT_FORMAT = "%s(): result of db.delete() from %s = %d";
    private final static String INSERT_CONTENT_VALUES_FORMAT = "%s(): contentValues for %s: %s";
    // ...

    public void updateOrCreateWatchlist(String name, long[] securityIds,
                                        long watchlistId) {
        final String methodName = "updateOrCreateWatchlist";
        Long insertResult = null;
        String[] whereArgs = new String[] { String.valueOf(watchlistId) };
        SQLiteDatabase db = getWritableDatabase();
        try {
            db.beginTransaction();
            // Save watchlist data
            boolean isExistingWatchlist = (watchlistId != NEW_ITEM_ID);
            ContentValues contentValues = new ContentValues();
            contentValues.put(Watchlist.NAME, name);
            if (isExistingWatchlist) {
                Integer updateResult = db.update(Watchlist.TABLE,
                        contentValues, Watchlist.ID + " = ?", whereArgs);
                Log.v(CLASS_NAME, String.format(UPDATE_RESULT_FORMAT,
                        methodName, Watchlist.TABLE, updateResult));
            } else {
                insertResult = db.insert(Watchlist.TABLE, null, contentValues);
                Log.v(CLASS_NAME, String.format(INSERT_RESULT_FORMAT,
                        methodName, Watchlist.TABLE, insertResult));
                watchlistId = insertResult;
                Log.v(CLASS_NAME, String.format("%s(): new watchlistId = %d",
                        methodName, watchlistId));
            }
            // Delete existing connections to securities
            if (isExistingWatchlist) {
                Integer deleteResult = db.delete(
                        SecuritiesInWatchlists.TABLE,
                        SecuritiesInWatchlists.WATCHLIST_ID + " = ?", whereArgs);
                Log.v(CLASS_NAME, String.format(DELETE_RESULT_FORMAT,
                        methodName, SecuritiesInWatchlists.TABLE,
                        deleteResult));
            } else {
                Log.v(CLASS_NAME, String.format(
                        "%s(): New watchlist; skipping delete in %s",
                        methodName, SecuritiesInWatchlists.TABLE));
            }
            // Create specified connections to securities
            contentValues = new ContentValues();
            for (int i = 0; i < securityIds.length; i++) {
                contentValues.clear();
                contentValues.put(SecuritiesInWatchlists.SECURITY_ID, securityIds[i]);
                contentValues.put(SecuritiesInWatchlists.WATCHLIST_ID, watchlistId);
                Log.v(CLASS_NAME, String.format(INSERT_CONTENT_VALUES_FORMAT,
                        methodName, SecuritiesInWatchlists.TABLE,
                        contentValues));
                insertResult = db.insert(SecuritiesInWatchlists.TABLE, null, contentValues);
                Log.v(CLASS_NAME, String.format(INSERT_RESULT_FORMAT,
                        methodName, SecuritiesInWatchlists.TABLE, insertResult));
            }
            db.setTransactionSuccessful();
            Log.d(CLASS_NAME, methodName + "(): success!");
        } finally {
            db.endTransaction();
        }
    } // updateOrCreateWatchlist()
}

For a new watchlist updateOrCreateWatchlist() first inserts it into the watchlist table and then creates the connection to its securities in the securities_in_watchlists table. All of this is done in a transaction of course. We’ll ignore the code to update existing watchlists for now.

Now WatchlistManagementActivity needs code to deal with tapping OK in WatchlistEditActivity:

public class WatchlistsManagementActivity extends AppCompatActivity {
    // ...

    @Override
    public void onActivityResult(int requestCode, int resultCode, Intent data) {
        super.onActivityResult(requestCode, resultCode, data);
        if (resultCode == RESULT_OK) {
            refreshWatchlistsListView();
        }
    } // onActivityResult()

    // ...

    private void refreshWatchlistsListView() {
        Cursor cursor = this.dbHelper.readAllWatchlists();
        this.watchlistManagementCursorAdapter.changeCursor(cursor);
    } // refreshWatchlistsListView()
}

Try the new functionality:

  1. Open the Manage Watchlists screen
  2. Tap New to show the Edit Watchlist screen – it says “Create Watchlist”
  3. Enter a name and select one or more securities
  4. Tap OK
  5. Also tap OK in the Manage Watchlists screen
  6. DbTradeAlert shows the new watchlist in the rightmost tab
  7. Optional: commit changes

What’s missing now is only functionality to edit and delete existing watchlists.

1.5 Add Edit Functionality for Existing Watchlists

Implementing functionality to edit watchlists is a bit involved because the button for it (and the one for deleting watchlists) isn’t in an activity like all the previous buttons. Instead, a ListView hosts a list of views and each view contains both a button for deleting and one for editing the watchlist it represents. For that reason the button’s click handler goes into the ListView’s CursorAdapter class which connects it in newView():

public class WatchlistManagementCursorAdapter extends CursorAdapter {
    // ...

    private View.OnClickListener editButtonClickListener = new View.OnClickListener() {

        public void onClick(View v) {
            WatchlistManagementDetailViewHolder holder
                    = (WatchlistManagementDetailViewHolder) ((View) v.getParent()).getTag();
            long watchListId = holder.watchListId;
            Intent intent = new Intent(holder.context, WatchlistEditActivity.class);
            intent.putExtra(WatchlistEditActivity.WATCHLIST_ID_INTENT_EXTRA, watchListId);
            ((Activity) holder.context).startActivityForResult(intent,
                    WatchlistEditActivity.UPDATE_WATCHLIST_REQUEST_CODE);
        }
    }; // editButtonClickListener

    // ...

    @Override
    public View newView(Context context, Cursor cursor, ViewGroup parent) {
        View view = View.inflate(context, R.layout.layout_watchlists_management_detail, null);
        // could replace WatchListManagementDetailViewHolder in ICS and above with
        // view.setTag(R.id.my_view, myView);
        WatchListManagementDetailViewHolder holder = new WatchListManagementDetailViewHolder();
        holder.context = context;
        holder.deleteButton = (Button) view.findViewById(R.id.deleteButton);
        //holder.deleteButton.setOnClickListener(deleteButtonClickListener);
        holder.editButton = (Button) view.findViewById(R.id.editButton);
        holder.editButton.setOnClickListener(editButtonClickListener);
        holder.nameTextView = (TextView) view.findViewById(R.id.nameTextView);
        holder.watchListId = cursor.getLong(cursor.getColumnIndex(WatchlistContract.Watchlist.ID));
        view.setTag(holder);
        return view;
    } // newView()


    public class WatchListManagementDetailViewHolder {
        public Context context;
        public Button deleteButton;
        public Button editButton;
        public TextView nameTextView;
        public long watchListId;
    } // class WatchListManagementDetailViewHolder
} // class WatchlistManagementCursorAdapter

The code creates an OnClickListener instance from an anonymous class and provides an onClick() handler for it. The view representing the watchlist is passed as a parameter to onClick() and the WatchlistManagementDetailViewHolder instance connected to that view provides access to the watchlist’s Id and WatchlistsManagementActivity’s Context (see highlighted lines).

A necessary change for that was to add a Context field to WatchlistManagementDetailViewHolder. That makes it possible to access the WatchlistsManagementActivity instance inside editButtonClickListener.

The code then creates an intent to show an WatchlistEditActivity and provides the watchlist’s Id in its extras so WatchlistEditActivity knows which watchlist to load. When starting the activity the code uses WatchlistEditActivity.UPDATE_WATCHLIST_REQUEST_CODE to signal that it’s about editing an existing watchlist.

Actually WatchlistEditActivity.onCreate() only checks if watchlistId isn’t DbHelper.NewItemId to decide if that’s an edit:

public class WatchlistEditActivity extends AppCompatActivity {
    public final static int UPDATE_WATCHLIST_REQUEST_CODE = 1;
    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        final String methodName = "onCreate";
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_watchlist_edit);
        this.dbHelper = new DbHelper(this);
        Bundle extras = getIntent().getExtras();
        if (extras != null) {
            this.watchlistId = extras.getLong(WATCHLIST_ID_INTENT_EXTRA);
            EditText nameEditText = (EditText) findViewById(R.id.nameEditText);
            if (this.watchlistId == DbHelper.NewItemId) {
                // Create mode
                nameEditText.setText("");
                setTitle("Create Watchlist");
            } else {
                // Update mode
                Cursor watchlistCursor = this.dbHelper.readWatchlist(this.watchlistId);
                if (watchlistCursor.getCount() == 1) {
                    watchlistCursor.moveToFirst();
                    nameEditText
                            .setText(watchlistCursor.getString(watchlistCursor
                                    .getColumnIndex(WatchlistContract.Watchlist.NAME)));
                } else {
                    Log.e(CLASS_NAME, String.format(
                            "%s(): readWatchlist() found %d watchlists with id = %d; expected 1!",
                            methodName, watchlistCursor.getCount(),
                            this.watchlistId));
                }
            }
        }
        refreshSecuritiesList(this.watchlistId);
    } // onCreate()

    // ...
}

It displays the watchlist’s name which is the only field provided by DbHelper.readWatchlist() and shows its securities.

public class DbHelper extends SQLiteOpenHelper {
    // ...

    private void logSql(String methodName, String[] columns, String orderBy,
                        String selection, String[] selectionArgs, String table) {
        StringBuilder sb = new StringBuilder();
        sb.append("SELECT ");
        if (columns != null) {
            for (int i = 0; i < columns.length; i++) {
                sb.append(columns[i]);
                if (i < columns.length - 1) {
                    sb.append(", ");
                }
            }
        } else {
            sb.append("*");
        }
        sb.append("\nFROM ");
        sb.append(table);
        if (TextUtils.isEmpty(selection) == false) {
            sb.append("\nWHERE ");
            sb.append(insertSelectionArgs(selection, selectionArgs));
        }
        if (TextUtils.isEmpty(orderBy) == false) {
            sb.append("\nORDER BY ");
            sb.append(orderBy);
        }
        Log.v(CLASS_NAME, methodName + "(): " + sb.toString());
    } // logSql()

    // ...

    public Cursor readWatchlist(long watchlistId) {
        final String methodName = "readWatchlist";
        Cursor cursor = null;
        Log.v(CLASS_NAME,
                String.format("%s(): watchlistId = %d", methodName, watchlistId));
        SQLiteDatabase db = getReadableDatabase();
        String selection = Watchlist.ID + " = ?";
        String[] selectionArgs = new String[]{String.valueOf(watchlistId)};
        String table = Watchlist.TABLE;
        logSql(methodName, null, null, selection, selectionArgs, table);
        cursor = db.query(table, null, selection, selectionArgs, null, null,
                null);
        Log.v(CLASS_NAME, String.format(CURSOR_COUNT_FORMAT, methodName, cursor.getCount()));
        if (cursor.getCount() != 1) {
            Log.e(CLASS_NAME, String.format(
                    "%s(): found %d watchlists with id = %d; expected 1!", methodName,
                    cursor.getCount(), watchlistId));
        }
        return cursor;
    } // readWatchlist()

    // ...
}

When the user taps OK the watchlist and its connection to securities are saved like for a new watchlist. And finally WatchlistsManagementActivity.onActivityResult() receives a resultCode of RESULT_OK and refreshes its list of watchlists to show a possibly changed name – again this was already implemented for creating new watchlists.

Give it a try:

  1. Open the Manage Watchlists screen
  2. Tap Edit on one of the watchlists to show the Edit Watchlist screen – it says “Edit Watchlist”
  3. Change the name or the securities to include
  4. Tap OK
  5. Also tap OK in the Manage Watchlists screen
  6. Check if the changes were applied correctly – either in the main screen or by using the Edit Watchlist screen again
  7. Optional: commit changes

1.6 Add Delete Functionality for Watchlists

The functionality to delete watchlists in WatchlistManagementCursorAdapter uses the same pattern as for editing them:

public class WatchlistManagementCursorAdapter extends CursorAdapter {
    public static final String WATCHLIST_DELETED_BROADCAST = "WatchlistDeletedBroadcast";
    // ...

    private View.OnClickListener deleteButtonClickListener = new View.OnClickListener() {

        public void onClick(final View v) {
            WatchListManagementDetailViewHolder holder
                    = (WatchListManagementDetailViewHolder) ((View) v.getParent()).getTag();
            String watchListName = holder.nameTextView.getText().toString();
            new AlertDialog.Builder(holder.context)
                    .setTitle("Delete?")
                    .setMessage(
                            String.format(
                                    "Delete watchlist '%s' and its connections to securities?",
                                    watchListName))
                    .setIcon(android.R.drawable.ic_dialog_alert)
                    .setPositiveButton(android.R.string.yes,
                            new DialogInterface.OnClickListener() {
                                public void onClick(DialogInterface dialog,
                                                    int whichButton) {
                                    WatchListManagementDetailViewHolder holder
                                            = (WatchListManagementDetailViewHolder) ((View) v
                                            .getParent()).getTag();
                                    long watchListId = holder.watchListId;
                                    dbHelper.deleteWatchlist(watchListId);
                                    Intent intent = new Intent(WATCHLIST_DELETED_BROADCAST);
                                    LocalBroadcastManager.getInstance(holder.context)
                                            .sendBroadcast(intent);
                                }
                            })
                    .setNegativeButton("Cancel", null)
                    .show();
        } // onClick()

    }; // deleteButtonClickListener

    // ...
}

If you aren’t used to Java the code for deleteButtonClickListener probably looks somewhat weird due to the anonymous classes and the builder pattern. The code itself is like the code to edit a watchlist but with an added twist: the user needs to tap OK in a confirmation dialog to actually delete a watchlist.

Creating that AlertDialog uses the builder pattern / a fluent API / method chaining. That’s syntactical sugar claiming to make the code more readable. Or maybe it makes it even less readable – you decide.

The setPositiveButton() method’s 2nd parameter is yet another OnClickListener and again it is provided as an anonymous class that only implements an onClick() handler. That handler determines the watchlist’s ID and passes it to DbHelper.deleteWatchlist(). After that it initiates a refresh of the list from which the watchlist was deleted.

WatchlistManagementCursorAdapter avoids referencing WatchlistsManagementActivity because that activity already uses the CursorAdapter. And because deleting a watchlist doesn’t show a new activity it can’t trigger WatchlistsManagementActivity.onActivityResult(). Again, time to send a local broadcast.

In WatchlistsManagementActivity a receiver for that broadcast has to be implemented, registered, and unregistered. Note that in lifecycle methods like onPause() and onResume() you always call super first.

public class WatchlistsManagementActivity extends AppCompatActivity {
    // ...

    private BroadcastReceiver watchlistDeletedBroadcastReceiver = new BroadcastReceiver() {
        @Override
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(
                    WatchlistsManagementCursorAdapter.WATCHLIST_DELETED_BROADCAST)) {
                refreshWatchlistsListView();
            }
        }
    }; // watchlistDeletedBroadcastReceiver

    // ...

    @Override
    public void onPause() {
        super.onPause();
        // Unregister broadcast receiver for WATCHLIST_DELETED_BROADCAST
        LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
        broadcastManager.unregisterReceiver(watchlistDeletedBroadcastReceiver);
    } // onPause()

    @Override
    public void onResume() {
        super.onResume();
        // Register broadcast receiver for WATCHLIST_DELETED_BROADCAST
        LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(this);
        IntentFilter intentFilter = new IntentFilter();
        intentFilter.addAction(WatchlistsManagementCursorAdapter.WATCHLIST_DELETED_BROADCAST);
        broadcastManager.registerReceiver(watchlistDeletedBroadcastReceiver, intentFilter);
    } // onResume()

    // ...
}

Deleting the data is straightforward – DbHelper.deleteWatchlist() first deletes the records connecting the watchlist to any securities and after that deletes the watchlist itself. Of course it wraps everything in a transaction. The log entries look like this if the watchlist showed 2 securities:
… V/DbHelper: deleteWatchlist(): watchlistId = 4
… V/DbHelper: deleteWatchlist(): result of db.delete() from securities_in_watchlists = 2
… V/DbHelper: deleteWatchlist(): result of db.delete() from watchlist = 1
… D/DbHelper: deleteWatchlist(): success!

Deleting a watchlist

Deleting a watchlist

To try it:

  1. Open the Manage Watchlists screen
  2. Tap Delete on one of the watchlists
  3. Tap Ok in the confirmation dialog – note that it displays the watchlist’s name to avoid any mishaps
  4. Also tap OK in the Manage Watchlists screen
  5. Check if the watchlist is gone
  6. Optional: commit changes

Next post: DbTradeAlert for Android: Add Security and Watchlist Management – Part 3

Advertisements
This entry was posted in Uncategorized and tagged , . Bookmark the permalink.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s