ClassNotFoundException when Deploying without Android Studio

Have you ever wondered why you didn’t have to set multiDexEnabled true anymore despite adding almost any library to an Android project will extend it to more than 64K methods?

Well, Android Studio conveniently injected that setting when deploying a debug version of your app. That also means a manual deploy for example via adb install ... will result in an error like this:


java.lang.RuntimeException: Unable to instantiate application ...: java.lang.ClassNotFoundException: Didn't find class "..." on path: DexPathList[zip file "/data/app...apk"],nativeLibraryDirectories=[/vendor/lib, /system/lib]]

While it’s a ClassNotFoundException the “DexPathList” gives away the true reason. So just add multiDexEnabled true like you always did.

If the next adb install ... results in the same error you fell for the same trap I did: Android Studio doesn’t create the .apk file when building via Build | Make Project. Select Build | Build APK instead.

Additional Resources

Advertisements
Posted in Uncategorized | Leave a comment

Data Binding Android ListViews

Innocent UI – but look behind the mask!

At I/O 2015 Google showed data binding to the Android developing masses. It basically generates the boilerplate code everybody had to type in previously to get data to the UI and back.

But while there are samples for about every detail of the technology there was none (*) that showed how to data bind a ListView – so here’s a step by step recipe.

Note that this post uses Android Studio 3.0 which comes with new defaults to generate code and layouts.

1: Enable Data Binding

The first step to data binding always is to enable it in the module’s build.gradle file:

apply plugin: 'com.android.application'

android {
    compileSdkVersion 26
    dataBinding {
        enabled = true
    }
    defaultConfig {
        // ...
    }
    // ...
}
// ...

2: Create the Data Model Class

A data model is just a container for the data to surface and you probably use this type of class regardless of data binding already. In this simple demo the data won’t change and a POJO will do:

// ...
public class DemoData {
    Date mTimestamp;

    public DemoData() {
        mTimestamp = new Date();
    }

    public String getTimestamp() {
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return sdf.format(mTimestamp);
    }
}

The only surprise is that getTimestamp() doesn’t return a Date but a String. That’s because the value will be bound to a property of type String and Android does no type conversions. Using the Date would result in LoggedErrorException - Cannot find the setter for attribute 'android:text' with parameter type java.util.Date on android.widget.TextView.

By the way: returning an Integer would result in a befuddling android.content.res.Resources$NotFoundException: String resource ID #0x0. That’s because setText() has an overload which takes an Integer and treats it as a resource ID.

And a production app will probably create the SimpleDateFormat instance only once.

3: Create the List Item Layout

The list item layout defines how each of the ListView’s items should look. When Android Studio 3 generates a new layout resource file it looks like this – finally a ConstraintLayout but no traces of data binding:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout      
    xmlns:android="http://schemas.android.com/apk/res/android"      
    android:layout_width="match_parent"      
    android:layout_height="match_parent">

</android.support.constraint.ConstraintLayout>

Using data binding takes four manual steps – see the result below:

  1. wrap the generated xml in a layout element (lines 2 to 5 and 21)
  2. specify the type of data to bind – in this case the previous section’s data model (lines 6 to 8)
  3. add UI elements (lines 12 to 19)
  4. bind UI elements to data (line 16)
<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"      
    xmlns:app="http://schemas.android.com/apk/res-auto"      
    xmlns:tools="http://schemas.android.com/tools">
    <data>
        <variable name="demoData" type="de.dbremes.listviewdatabinding.DemoData"/>
    </data>
    <android.support.constraint.ConstraintLayout          
        android:layout_width="match_parent"          
        android:layout_height="match_parent">
        <TextView
            android:id="@+id/positionTextView"              
            android:layout_width="wrap_content"                          
            android:layout_height="wrap_content"              
            android:text="@{demoData.timestamp}"              
            app:layout_constraintTop_toBottomOf="parent"              
            app:layout_constraintStart_toStartOf="parent"              
            tools:text="1: 2017-11-05 14:41:36"/>
    </android.support.constraint.ConstraintLayout>
</layout>

The most important part isn’t even shown: this layout is called layout_demo_item.xml and will result in a data binding class named LayoutDemoItemBinding once you compile the project.

Had the data model simply returned a Date you’d have to convert it here: @{String.valueOf(demoData.timestamp)}.

Note that IDs are only used for constraints and state management but not needed for data binding.

4: Create the Layout with the ListView using the List Items

Data binding the ListView itself consists of the same steps as creating the list item layout. This example starts from Android Studio’s Basic Activity template which provides this layout in content_main.xml:

<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout      
    xmlns:android="http://schemas.android.com/apk/res/android"      
    xmlns:app="http://schemas.android.com/apk/res-auto"      
    xmlns:tools="http://schemas.android.com/tools"      
    android:layout_width="match_parent"      
    android:layout_height="match_parent"      
    app:layout_behavior="@string/appbar_scrolling_view_behavior"      
    tools:context="de.dbremes.listvewdatabinding.MainActivity"      
    tools:showIn="@layout/activity_main">

    <TextView      android:layout_width="wrap_content"      
        android:layout_height="wrap_content"
        android:text="Hello World!"      
        app:layout_constraintBottom_toBottomOf="parent"      
        app:layout_constraintLeft_toLeftOf="parent"      
        app:layout_constraintRight_toRightOf="parent"      
        app:layout_constraintTop_toTopOf="parent" />

</android.support.constraint.ConstraintLayout>

Result after completing the required steps:

<?xml version="1.0" encoding="utf-8"?>
<layout
    xmlns:android="http://schemas.android.com/apk/res/android"      
    xmlns:app="http://schemas.android.com/apk/res-auto"      
    xmlns:tools="http://schemas.android.com/tools">

    <android.support.constraint.ConstraintLayout          
        android:layout_width="match_parent"          
        android:layout_height="match_parent"          
        app:layout_behavior="@string/appbar_scrolling_view_behavior"          
        tools:context="de.dbremes.listviewdatabinding.MainActivity"          
        tools:showIn="@layout/activity_main">

        <ListView
            android:id="@+id/demoListView"              
            android:layout_width="match_parent"              
            android:layout_height="match_parent"              
            android:layout_marginLeft="8dp"              
            android:layout_marginRight="8dp"              
            android:layout_marginTop="8dp"              
            app:layout_constraintLeft_toLeftOf="parent"              
            app:layout_constraintTop_toTopOf="parent"              
            tools:listitem="@layout/layout_demo_item"/>

    </android.support.constraint.ConstraintLayout>
</layout>

Note line 23 which shows sample list view items – slick!

5: Create an Adapter Class

An adapter connects data a.k.a. data model and UI. Because a ListView provides the UI a subclass of BaseAdapter is required.

Once you create a class extending BaseAdapter Android Studio will volunteer to complete it with the four methods shown:

package de.dbremes.listviewdatabinding;

import android.view.View;
import android.view.ViewGroup;
import android.widget.BaseAdapter;

public class DemoAdapter extends BaseAdapter {
    @Override
    public int getCount() {
        return 0;
    }

    @Override
    public Object getItem(int i) {
        return null;
    }

    @Override
    public long getItemId(int i) {
        return 0;
    }

    @Override
    public View getView(int i, View view, ViewGroup viewGroup) {
        return null;
    }
}

The first three methods just need a list of DemoModel instances to work and getView() is where things get interesting. This is the completed class:

public class DemoAdapter extends BaseAdapter {
    private ArrayList<DemoData> mDemoModels;
    private LayoutInflater mLayoutInflater;

    DemoAdapter(ArrayList<DemoData> demoModels) {
        mDemoModels = demoModels;
    } // ctor()

    // @Override
    public int getCount() {
        return mDemoModels.size();
    }

    @Override
    public Object getItem(int i) {
        return mDemoModels.get(i);
    }

    @Override
    public long getItemId(int i) {
        return i;
    }

    @Override
    public View getView(final int i, View view, final ViewGroup viewGroup) {
        View result = view;
        LayoutDemoItemBinding binding;
        if (result == null) {
            if (mLayoutInflater == null) {
                mLayoutInflater = (LayoutInflater) viewGroup.getContext()
                        .getSystemService(Context.LAYOUT_INFLATER_SERVICE);
            }
            binding = LayoutDemoItemBinding.inflate(
                    mLayoutInflater, viewGroup, false);
            result = binding.getRoot();
            result.setTag(binding);
        }
        else {
            binding = (LayoutDemoItemBinding) result.getTag();
        }
        result.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View v) {
                Context context = viewGroup.getContext();
                Toast toast = Toast.makeText(context,
                        "Position=" + Integer.toString(i) + ": " + ((DemoData)getItem(i)).getTimestamp(),
                        Toast.LENGTH_SHORT);
                toast.show();
            }
        });
        binding.setDemoData(mDemoModels.get(i));
        return result;
    }
}

The constructor just takes and stores the data. Then getView() is called for each of the ListView’s elements:

  1. i provides the element’s position – why should the parameter’s name give that away?
  2. view is a used view representing a list item layout or null. If it’s a view the method just updates it with the respective data.
  3. viewGroup is the parent of the view to return

If getView() doesn’t get a view to reuse the LayoutDemoItemBinding class previously generated from layout_demo_item.xml creates the proper binding. That gets provided with the data and stored in the resulting view’s tag.

Basic data binding is accompanied by showing how to handle click events on items in lines 41 to 50.

6: Connect Adapter Class and ListView

Connecting Adapter Class and ListView is done in MainActivity’s OnCreate() handler:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ActivityMainBinding binding
                = DataBindingUtil.setContentView(this, R.layout.activity_main);
        final ArrayList<DemoData> demoDataList = new ArrayList<>();
        demoDataList.add(new DemoData());
        final DemoAdapter adapter = new DemoAdapter(demoDataList);
        // This requires
        // - adding a layout element to activity_main.xml
        // - adding an id to the existing include element
        binding.contentMain.demoListView.setAdapter(adapter);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @Override
            public void onClick(View view) {
                demoDataList.add(new DemoData());
                adapter.notifyDataSetChanged();;
            }
        });
    }

Lines 4 and 14 to 24 come with the default implementation. After that:

  1. instantiate the binding (line 5, 6)
  2. instantiate the adapter (line 9)
  3. plug it all together (line 13)

Android Studio’s Basic Activity template comes with activity_main.xml which has an include element for content_main.xml. To make data binding in content_main.xml work you have to:

  • wrap activity_main.xml in a layout element like shown for the other layouts
  • add an id attribute to activity_main.xml’s include element for content_main.xml – android:id=”@+id/content_main” was used in this case leading to a binding named contentMain

Additional Resources

 

Posted in Uncategorized | Tagged | Leave a comment

Styling Android Toasts the Easy Way

The first step to style Android Toasts is to define a layout, right? Well often it’s not, because to just show text and an image – yes, the built-in Toast can show an image – you don’t need a layout.

But let’s get another common misconception about Toast layouts out of the way before getting rid of them altogether. That misconception is even in Google’s own sample code which looks like this:

LayoutInflater inflater = getLayoutInflater();
View layout = inflater.inflate(R.layout.custom_toast,
                (ViewGroup) findViewById(R.id.custom_toast_container));

TextView text = (TextView) layout.findViewById(R.id.text);
text.setText("This is a custom toast");

Toast toast = new Toast(getApplicationContext());
toast.setGravity(Gravity.CENTER_VERTICAL, 0, 0);
toast.setDuration(Toast.LENGTH_LONG);
toast.setView(layout);
toast.show();

The thing is (ViewGroup) findViewById(R.id.custom_toast_container) will always return null because the Activity has no idea of R.id.custom_toast_container.

But just calling it like View layout = inflater.inflate(R.layout.custom_toast, null); causes a Lint warning “Avoid passing null as the view root …”. That’s usually correct because while the inflater will still do its job the configuration information would get lost which may or may not lead to a messed up layout.

This doesn’t apply to Toasts though and passing null is perfectly OK. So just suppress the Lint warning with @SuppressLint("InflateParams").

Now let’s take a look at this code that customizes Android’s built-in toast on-the-fly:

// ...
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
        setSupportActionBar(toolbar);

        FloatingActionButton fab = (FloatingActionButton) findViewById(R.id.fab);
        fab.setOnClickListener(new View.OnClickListener() {
            @RequiresApi(api = Build.VERSION_CODES.JELLY_BEAN_MR1)
            @Override
            public void onClick(View view) {
                Toast toast = Toast.makeText(
                        MainActivity.this, "Android loves toasts!", Toast.LENGTH_LONG);
                toast.setGravity(Gravity.CENTER, 0, 0);
                View toastView = toast.getView();
                // You could also add Views to toastView here or scale it
                toastView.setBackgroundColor(0x80FFFFFF);
                // Or keep toast color and just change opacity:
                //toastView.getBackground().setAlpha(128);
                TextView textView = (TextView) toastView.findViewById(android.R.id.message);
                textView.setAlpha(.5f);
                textView.setGravity(Gravity.CENTER_HORIZONTAL);
                textView.setTextSize(40);
                Drawable toastLoveDrawable
                        = ContextCompat.getDrawable(MainActivity.this, R.mipmap.ic_toast_love);
                toastLoveDrawable.setAlpha(128);
                // textView.setCompoundDrawables() doesn't show the drawable!
                textView.setCompoundDrawablesWithIntrinsicBounds(
                        null, toastLoveDrawable, null, null);
                toast.show();
            }
        });
    }
// ...

Its result looks like this – “pretty” was not in the requirements:

Lines 14 to 16 are standard code. But Line 17 shows how to access the built-in Toast’s layout. You can manipulate it like any other View object and in this case its background color and opacity are changed – more on that later.

Lines 22 to 31 show how to style the TextView. The straightforward part is setting its position, opacity and size.

The surprising part is that each TextView contains a means to show an image – certainly a debatable architectural decision but that’s another story. Again the code focuses on setting the opacity.

The code not shown is just Android Studio’s Basic Activity template with some color and text adjustments to get a proper background for showing a transparent Toast.

In lieu of witty closing words here’s how to calculate opacity / alpha values. The alpha value controls a View’s opacity. It’s beween 0 (completely transparent) and 1 (completely opaque). So textView.setAlpha(.5f) results in a half transparent TextView.

A Drawable’s alpha values work the same but are beween 0 and 255. So toastLoveDrawable.setAlpha(128) results in a half transparent Drawable. Because getBackground() returns a Drawable toastView.getBackground().setAlpha(128) works likewise.

A 3rd option to get transparent output is specifying the background color’s alpha channel. Values are beween 0 and 255 again but they are usually given as a hexadecimal number. And 128 equals 0x80 so toastView.setBackgroundColor(0x80FFFFFF) specifies a half-transparent white background.

Additional Resources

Posted in Uncategorized | Tagged | Leave a comment

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

First post in this series: Introduction to DbTradeAlert

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


2.4 Add Save Functionality for New Securities

Saving new securities is implemented like its watchlist counterpart with two additions:

  • Make shure date and float values were entered correctly
  • Stop the user from entering a duplicate symbol
public class SecurityEditActivity extends AppCompatActivity {
    // ...

    private Date getDateFromEditText(Integer editTextId) {
        Date result = null;
        EditText editText = (EditText) findViewById(editTextId);
        if (editText.length() > 0) {
            DateFormat dateFormat = DateFormat.getDateInstance(DateFormat.SHORT);
            String text = editText.getText().toString();
            try {
                result = dateFormat.parse(text);
            } catch (ParseException e) {
                Log.e(CLASS_NAME, Utils.EXCEPTION_CAUGHT, e);
                Toast.makeText(
                        this, "Error: '" + text + "' is not a valid date", Toast.LENGTH_SHORT)
                        .show();
            }
        }
        return result;
    } // getDateFromEditText()

    // ...

    public void onOkButtonClick(View view) {
        Float basePrice = getFloatFromEditText(R.id.basePriceEditText);
        Date basePriceDate = getDateFromEditText(R.id.basePriceDateEditText);
        Float lowerTarget = getFloatFromEditText(R.id.lowerTargetEditText);
        Float maxPrice = getFloatFromEditText(R.id.maxPriceEditText);
        Date maxPriceDate = getDateFromEditText(R.id.maxPriceDateEditText);
        String notes = Utils.getStringFromEditText(this, R.id.notesEditText);
        String symbol = Utils.getStringFromEditText(this, R.id.symbolEditText);
        Float upperTarget = getFloatFromEditText(R.id.upperTargetEditText);
        Float trailingTarget = getFloatFromEditText(R.id.trailingTargetEditText);
        long[] watchlistIds = Utils.getSelectedListViewItemIds(this, R.id.watchlistsListView);
        String errorMessage = this.dbHelper.updateOrCreateSecurity(basePrice, basePriceDate,
                lowerTarget, maxPrice, maxPriceDate, notes, this.securityId,
                symbol, trailingTarget, upperTarget, watchlistIds);
        if (TextUtils.isEmpty(errorMessage)) {
            setResult(RESULT_OK, getIntent());
            finish();
        } else {
            Toast.makeText(this, errorMessage, Toast.LENGTH_LONG).show();
        }
    } // onOkButtonClick()

    // ...
}

The SQL is similar to that saving watchlists with the only twist of avoiding duplicate symbols. DbHelper.updateOrCreateSecurity() first checks whether securityId equals NEW_ITEM_ID. If that’s the case it looks for an existing security with the same symbol. If it finds one it aborts the transaction and returns an error message.

SecurityEditActivity.onOkButtonClick() checks whether DbHelper.updateOrCreateSecurity() returned an non-empty string. If that’s the case it shows a Toast with that string and stays on screen.

Finally SecurityManagementActivity needs to be extended:

  • onActivityResult() calls refreshSecuritiesListView() if resultCode was RESULT_OK
  • refreshSecuritiesListView() does exactly what its name says

Try the new functionality:

  1. Open the Manage Securities screen
  2. Tap New to show the Edit Security screen – it says “Add Security”
  3. Enter a symbol that’s already in use and tap OK – you’ll get a message that you cannot add another security with that symbol and the Edit Security screen stays
  4. Enter a new symbol and select one or more watchlists
  5. Tap OK – the Edit Security screen closes and the Manage Securities screen shows the new security – without its name because no quotes were downloaded yet
  6. Also tap OK in the Manage Securities screen to close it
  7. Navigate to one of the watchlists that include the new security – there is no report for it because no quotes were downloaded yet
  8. Tap refresh – DbTradeAlert shows a complete report for the new security
  9. Optional: commit changes

2.5 Add Edit Functionality for Existing Securities

Editing a security - symbol read-only

Editing a security – symbol read-only

Editing securities is implemented like editing watchlists and I’ll skip repeating the explanation. One difference is that SecurityEditActivity.onCreate() calls symbolEditText.setEnabled(false) for existing securities. And DbHelper.readSecurity() of course has to join the Quote table for its Name field.

When the user taps OK the security and its connection to watchlists are saved like for a new security. If the user entered a duplicate symbol the error message from DbHelper is shown and the screen stays open.

And finally WatchlistsManagementActivity.onActivityResult() receives a resultCode of RESULT_OK and refreshes is list of securities if this was an addition – as it shows only the symbols and these are immutable there is no need for a refresh after updating a security.

Give it a try:

  1. Open the Manage Securities screen
  2. Tap Edit on one of the securities to show the Edit Security screen – it says “Edit Security”
  3. Change one of the fields or the watchlists that will include the security
  4. Tap OK
  5. Also tap OK in the Manage Securities screen
  6. Check if the changes were applied correctly
  7. Optional: commit changes

2.6 Add Delete Functionality for Securities

The functionality to delete securities uses the same pattern as for editing them – for explanations see deleting watchlists.

Deleting the data is straightforward – DbHelper.deleteSecurity() first deletes the security’s quote, then the records connecting the security to any watchlists and after that deletes the security itself. Of course it wraps everything in a transaction. The log entries look like this if the security was in a single watchlist:
… V/DbHelper: deleteSecurity(): securityId = 6
… V/DbHelper: deleteSecurity(): result of db.delete() from quote = 1
… V/DbHelper: deleteSecurity(): result of db.delete() from securities_in_watchlists = 1
… V/DbHelper: deleteSecurity(): result of db.delete() from security = 1
… D/DbHelper: deleteSecurity(): success!

Deleting a security

Deleting a security

To try it:

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

Next post: Add Reminders

Posted in Uncategorized | Tagged , | Leave a comment

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

First post in this series: Introduction to DbTradeAlert

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


2. Add Securities Management

Securities management works exactly the same way watchlists management does. And adding securities management will follow the exact same steps adding watchlist management used. So let’s just fast forward through this.

2.1 Add the Manage Securities Screen

You need a new empty activity named “SecuritiesManagementActivity” with 3 Buttons, a ListView, and a TextView similar to activity_watchlists_management.xml.

And you need code for the Cancel and OK buttons and to set the screen’s title in onCreate() like in WatchlistsManagementActivity.java.

And finally you need a new menu item in menu_watchlist_list.xml.

Then you need to extend WatchlistListActivity to start the new activity in onOptionsItemSelected() and retrieve its result in onActivityResult().

Manage Securities screen - list of watchlists empty

Manage Securities screen – list of watchlists empty

Now try out the additions:

  1. Start the app
  2. In its overflow menu tap “Manage Securities”
  3. The “Manage Securities” screen appears
    1. Note the hint about adding a new security when there is none
    2. Note that there is no menu but a distinct title for the screen
  4. In the “Manage Securities” screen tap either OK or Cancel – tapping “New” will crash the app
  5. Optional: commit the changes

2.2 Fill List of Existing Securities

To list the existing securities DbTradeAlert needs a layout to show each security. It’s added like layout_watchlists_management_detail with one more TextView to show the security’s symbol.

Manage Securities screen listing securities

Manage Securities screen listing securities

The next step will be using an adapter to marry the ListView with its cursor and its detail layout. So create a new class named “SecuritiesManagementCursorAdapter” extending CursorAdapter like you did for WatchlistsManagementCursorAdapter.

For explanations see filling the list of existing watchlists.

Finally connect SecuritiesManagementCursorAdapter to securitiesListView in WatchlistsManagementActivity.onCreate() – to fill the list of securities DbHelper.getAllSecuritiesAndMarkIfInWatchlist() will be reused.

Test it: the securities show up in the Manage Securities screen. Optionally check in the changes.

2.3 Add an Activity to Edit Securities

Add a new empty activity named “SecurityEditActivity”. It hosts a lot of controls and I’ll just show the resulting layout:

Edit Security layout

Edit Security layout

The first step after creating the layout is to provide a handler for SecuritiesManagementActivity’s New button. The pattern is exactly like in WatchlistsManagementActivity:

  1. Create an intent pointing to SecurityEditActivity.class
  2. Add an Extra named SecurityEditActivity.SECURITY_ID_INTENT_EXTRA with the security ID of DbHelper.NEW_ITEM_ID to the intent
  3. Call startActivityForResult() passing the intent and SecurityEditActivity.CREATE_SECURITY_REQUEST_CODE

And SecurityEditActivity’s required extensions:

  1. It needs definitions for CREATE_SECURITY_REQUEST_CODE and SECURITY_ID_INTENT_EXTRA
  2. onCancelButtonClick():
    1. Sets result to RESULT_CANCELED
    2. Closes the screen
  3. onCreate():
    1. Grabs the ID from the intent’s Extras
    2. Clears the EditTexts for new securities
    3. Sets the screen’s title
    4. Calls refreshSecuritiesListView() passing the security’s ID
  4. refreshSecuritiesListView():
    1. Calls dbHelper.getAllWatchlistsAndMarkIfSecurityIsIncluded() passing the security’s ID
    2. Creates a SimpleCursorAdapter instance to connect cursor and ListView
    3. Sets the ListView’s EmptyView
    4. Marks the watchlists that include the currently shown security

Again this code works exactly like that for editing a watchlist – just with a lot more fields.

Finally DbHelper needs getAllWatchlistsAndMarkIfSecurityIsIncluded() which works like explained for DbHelper.getAllSecuritiesAndMarkIfInWatchlist().

Run the app again:

  1. Open the Manage Securities screen
  2. Tap New: the new Edit Security screen shows up listing all watchlists and ready for input
    1. again no menu but a distinct title of “Add Security”
  3. Tap Cancel – tapping OK will crash the app
  4. Optional: commit changes
Adding a new security

Adding a new security

The screen shows a security’s fields: at the top is the symbol field. Below that you see value and date for base and maximum price, targets, and the notes field. Note that the dates have to be entered according to the device’s locale. While Android provides a DatePickerDialog I found it too much hassle to implement and connect it to the EditTexts.

At the bottom of the screen is a list of all watchlists with those checked that contain this security.

Note that that I closed the automatically displayed on-screen keyboard for the screenshot so it doesn’t obstruct the activity’s screen.

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

Additional Resources:

Posted in Uncategorized | Tagged , | Leave a comment

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

Posted in Uncategorized | Tagged , | Leave a comment

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

First post in this series: Introduction to DbTradeAlert

Previous post: DbTradeAlert for Android: Schedule Quote Updates – Part 3


By now DbTradeAlert does what it’s supposed to do – inform the user about triggered signals. What’s missing is the ability to manage its entities:

  • Add, edit and delete securities
  • Add, edit and delete watchlists

For both entities the adding and deleting will be started in one activity and the actual editing will be done in a second activity.

Managing watchlists is easier so let’s start with this.

1. Add Watchlists Management

The first step will be to create the Watchlists Management screen and add code to invoke and dismiss it. In the Watchlists Management screen users see a list of all their watchlists and can Tap Edit or New to go to the – not yet existing – Edit Watchlist screen. They can also Tap Delete to get rid of an unused watchlist.

1.1 Add the Watchlists Management Screen

Add a new empty activity named “WatchlistsManagementActivity” (notice plural). After that add 3 Buttons, a ListView, and a TextView to activity_watchlists_management.xml:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingBottom="@dimen/activity_vertical_margin" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" tools:context=".WatchlistsManagementActivity">

    <Button android:id="@+id/newButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_centerHorizontal="true" android:onClick="onNewButtonClick" android:text="New" />

    <Button android:id="@+id/okButton" style="android:buttonStyle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentRight="true" android:onClick="onOkButtonClick" android:text="@android:string/ok" />

    <Button android:id="@+id/cancelButton" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentBottom="true" android:layout_alignParentLeft="true" android:onClick="onCancelButtonClick" android:text="Cancel" />

    <ListView android:id="@+id/watchlistsListView" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_above="@+id/okButton" android:layout_alignParentLeft="true" android:layout_below="@+id/newButton" />

    <TextView android:id="@+id/emptyTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentLeft="false" android:layout_alignParentTop="false" android:layout_centerInParent="true" android:text="(Click 'New' to create a new watchlist)" android:textAppearance="?android:attr/textAppearanceSmall" />
</RelativeLayout>

As the OK button shows Android provides translations for general strings like “OK”, “Cancel” or “New” that it will automatically use depending on the device’s language setting. But internationalizing an app properly is a huge effort which doesn’t make sense for DbTradeAlert and so I use hard-coded strings. The rest of the layout should look familiar by now.

Android Studio will automatically add the activity to AndroidManifest.xml.

Then add code for the Cancel and OK buttons and extend onCreate() to set the screen’s title. Using “android:label” in “layout/activity_watchlists_management.xml” would not work because “android:label” in “AndroidManifest.xml” will overrule it. setTitle() does the trick.

public class WatchlistsManagementActivity extends AppCompatActivity {
    // ...

    public void onCancelButtonClick(View view) {
        setResult(RESULT_CANCELED, getIntent());
        finish();
    } // onCancelButtonClick()

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_watchlists_management);
        setTitle("Manage Watchlists");
    } // onCreate()

    public void onOkButtonClick(View view) {
        setResult(RESULT_OK, getIntent());
        finish();
    } // onOkButtonClick()
}

Now add a new entry to the app’s menu:

And finally extend WatchlistListActivity to start the new activity and retrieve its result:

public class WatchlistListActivity extends AppCompatActivity
        implements WatchlistFragment.OnListFragmentInteractionListener {
    private static final int WATCHLISTS_MANAGEMENT_REQUEST = 2;
    private WatchlistListPagerAdapter watchlistListPagerAdapter;
    // ...

    @Override
    protected void onActivityResult(int requestCode, int resultCode, Intent data) {
        final String methodName = "onActivityResult";
        switch (requestCode) {
            case WATCHLISTS_MANAGEMENT_REQUEST:
                watchlistListPagerAdapter.notifyDataSetChanged();
                break;
            default:
                Log.e(CLASS_NAME, String.format("%s(): unexpected requestCode = ",
                        methodName, requestCode));
                break;
        }
        super.onActivityResult(requestCode, resultCode, data);
    } // onActivityResult()

    // ...

    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
        // Handle action bar item clicks here. The action bar will
        // automatically handle clicks on the Home/Up button, so long
        // as you specify a parent activity in AndroidManifest.xml.
        int id = item.getItemId();
        switch (id) {
            case R.id.action_refresh: {
                setTitle(APP_NAME);
                Context context = getApplicationContext();
                Intent service = new Intent(context, QuoteRefresherService.class);
                service.putExtra(QuoteRefresherService.INTENT_EXTRA_IS_MANUAL_REFRESH, true);
                startService(service);
                return true;
            }
            case R.id.action_watchlists_management: {
                Intent intent = new Intent(this, WatchlistsManagementActivity.class);
                startActivityForResult(intent, WATCHLISTS_MANAGEMENT_REQUEST);
                return true;
            }
            case R.id.action_settings: {
                return true;
            }
            default:
                return super.onOptionsItemSelected(item);
        }
    }

    // ...
}

In onOptionsItemSelected() you start the activity by calling the superclass’ startActivityForResult() method with an intent specifying the activity’s class. That method also takes an ID that will be passed back to the superclass’ onActivityResult() method once the user has clicked OK or Cancel in the activity. Regardless of resultCode onActivityResult() will initiate a refresh of watchlistListPagerAdapter because while the user may have tapped Cancel in the Manage Watchlists screen he may have OK’d changes in the Edit Watchlist screen.

Not informing watchlistListPagerAdapter about this will result in an exception: “IllegalStateException: The application’s PagerAdapter changed the adapter’s contents without calling PagerAdapter#notifyDataSetChanged!”

To make this possible watchlistListPagerAdapter’s declaration needs to be moved to class level.

Manage Watchlists screen with empty list

Manage Watchlists screen with empty list

Now try out the additions:

  1. Start the app
  2. In its overflow menu tap “Manage Watchlists” – the “Manage Watchlists” screen appears
  3. Note the hint about creating a new watchlist when there is none; that’s the only work emptyTextView will ever do
  4. Note that there is no menu but a distinct title for the screen
  5. In the “Manage Watchlists” screen tap either OK or Cancel – tapping “New” will crash the app
  6. Optional: commit the changes

1.2 Fill List of Existing Watchlists

The next step will be to list the available watchlists. For that DbTradeAlert needs a layout to show each watchlist. To add one:

  1. In the res/layout folder’s context menu select New | XML | Layout XML File
  2. In the Configure Component window:
    1. Layout File Name: “layout_watchlists_management_detail”
    2. Root Tag: “RelativeLayout”
    3. Click Finish

Complete the layout by adding 2 Buttons and a textView:

<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent">

    <Button android:id="@+id/editButton" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentRight="true" android:layout_alignParentTop="true" android:text="Edit" />

    <Button android:id="@+id/deleteButton" style="?android:attr/buttonStyleSmall" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignParentTop="true" android:layout_toLeftOf="@+id/editButton" android:text="Delete" />

    <TextView android:id="@+id/nameTextView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_alignBaseline="@+id/deleteButton" android:layout_alignBottom="@+id/deleteButton" android:layout_alignParentLeft="true" android:text="Default watch list" android:textAppearance="?android:attr/textAppearanceMedium" />

</RelativeLayout>

Like for the previously created lists you’ll need an adapter to marry the ListView with its cursor and its detail layout. Only this time it will be more involved because of the edit and delete actions provided by the layout.

Create a new class named “WatchlistManagementCursorAdapter” extending CursorAdapter (android.support.v4.widget.CursorAdapter). Let Android Studio create the missing methods and a constructor with “Context context, Cursor c, boolean autoRequery” parameters. The next step is to extend the class like shown below including the private WatchListManagementDetailViewHolder class:

package de.dbremes.dbtradealert;

import android.content.Context;
import android.database.Cursor;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.CursorAdapter;
import android.widget.TextView;

import de.dbremes.dbtradealert.DbAccess.DbHelper;
import de.dbremes.dbtradealert.DbAccess.WatchlistContract;

public class WatchListManagementCursorAdapter extends CursorAdapter {
    private Context context;
    DbHelper dbHelper;

    public WatchListManagementCursorAdapter(Context context, Cursor c, boolean autoRequery) {
        super(context, c, autoRequery);
        this.context = context;
        this.dbHelper = new DbHelper(this.context);
    } // ctor()

    @Override
    public void bindView(View view, Context context, Cursor cursor) {
        WatchListManagementDetailViewHolder holder
                = (WatchListManagementDetailViewHolder) view.getTag();
        holder.nameTextView.setText(cursor.getString(cursor
                .getColumnIndex(WatchlistContract.Watchlist.NAME)));
        holder.watchListId = cursor.getLong(cursor
                .getColumnIndex(WatchlistContract.Watchlist.ID));
    } // bindView()

    @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.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()


    private class WatchListManagementDetailViewHolder {
        public Button deleteButton;
        public Button editButton;
        public TextView nameTextView;
        public long watchListId;
    } // class WatchListManagementDetailViewHolder
} // class WatchListManagementCursorAdapter

As always the ViewHolder’s job is to save the time required for finding controls with findViewById(). newView() creates a new WatchListManagementDetailViewHolder instance, stores references to the controls in it and saves it to the View’s tag. After that bindView() is called and uses those references to update the control’s data. Because bindView() is called way more often than newView() the time savings from storing references to the views add up. Even more important: bindView() is called when the screen updates and that’s where split seconds count.

Finally connect WatchlistManagementCursorAdapter to watchListsListView in WatchlistsManagementActivity.onCreate(). Again, the pattern should be familiar by now.

public class WatchlistsManagementActivity extends AppCompatActivity {
    private Cursor cursor;
    private DbHelper dbHelper;
    private WatchListManagementCursorAdapter watchListManagementCursorAdapter
    // ...

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_watchlists_management);
        dbHelper = new DbHelper(this);
        this.cursor = dbHelper.readAllWatchlists();
        this.watchListManagementCursorAdapter
                = new WatchListManagementCursorAdapter(this, this.cursor, false);
        ListView watchListsListView = (ListView) findViewById(R.id.watchListsListView);
        TextView emptyTextView = (TextView) findViewById(R.id.emptyTextView);
        watchListsListView.setEmptyView(emptyTextView);
        watchListsListView.setAdapter(watchListManagementCursorAdapter);
    } // onCreate()

    // ...
}
Watchlists Management screen showing watchlists

Watchlists Management screen showing watchlists

Now start the app and test again: its watchlists show up in the Manage Watchlists screen – be aware that tapping the Edit or Delete buttons will crash the app. Again a good time to check in the changes.

A prerequisite for working New and Edit buttons is yet another new activity: one to edit a watchlist’s details. So let’s create that activity.

1.3 Add an Activity to Edit Watchlists

Add a new empty activity named “WatchlistEditActivity” (notice singular). The layout needs 2 Buttons, a ListView, a TextEdit, and 3 TextViews. That ends up being 250 lines of xml so I’ll not post it – just get the code from GitHub or look at the screenshot below.

The first step is to provide code for WatchlistsManagementActivity’s New button:

public class WatchlistsManagementActivity extends AppCompatActivity {
    // ...

    public void onNewButtonClick(View view) {
        Intent intent = new Intent(this, WatchlistEditActivity.class);
        intent.putExtra(WatchlistEditActivity.INTENT_EXTRA_WATCHLIST_ID,
                DbHelper.NEW_ITEM_ID);
        startActivityForResult(intent,
                WatchlistEditActivity.CREATE_WATCHLIST_REQUEST_CODE);
    } // onNewButtonClick()

} // class WatchlistsManagementActivity()

The pattern around startActivityForResult() is as used before. But this time an ID is transferred in the intent’s extras so the target activity knowns which watchlist to work on. As onNewButtonClick() starts the creation of a new watchlist it transfers a special ID that DbTradeAlert uses to signal a new item.

And WatchlistEditActivity’s required extensions:

public class WatchlistEditActivity extends AppCompatActivity {
    private final static String CLASS_NAME = "WatchlistEditActivity";
    public final static int CREATE_WATCHLIST_REQUEST_CODE = 0;
    public final static String INTENT_EXTRA_WATCHLIST_ID = "de.dbremes.dbtradealert.watchlistId";
    private DbHelper dbHelper;
    private long watchlistId = DbHelper.NewItemId;

    public void onCancelButtonClick(View view) {
        setResult(RESULT_CANCELED, new Intent());
        finish();
    } // onCancelButtonClick()

    @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(INTENT_EXTRA_WATCHLIST_ID);
            EditText nameEditText = (EditText) findViewById(R.id.nameEditText);
            if (this.watchlistId == DbHelper.NewItemId) {
                // Create mode
                nameEditText.setText("");
                setTitle("Create Watchlist");
            } 
        }
        refreshSecuritiesList(this.watchlistId);
    } // onCreate()

    private void refreshSecuritiesList(long watchListId) {
        final String methodName = "showStockList";
        SimpleCursorAdapter adapter;
        // Connect securities list to cursor
        String[] fromColumns = { SecurityContract.Security.SYMBOL };
        int[] toViews = { android.R.id.text1 };
        Cursor securitiesCursor = this.dbHelper.getAllSecuritiesAndMarkIfInWatchlist(watchListId);
        int flags =0;
        adapter = new SimpleCursorAdapter(this,
                android.R.layout.simple_list_item_multiple_choice,
                securitiesCursor, fromColumns, toViews, flags);
        ListView securitiesToIncludeListView
                = (ListView) findViewById(R.id.securitiesToIncludeListView);
        TextView emptyTextView = (TextView) findViewById(R.id.emptyTextView);
        securitiesToIncludeListView.setEmptyView(emptyTextView);
        securitiesToIncludeListView.setAdapter(adapter);
        // Mark securities that are included in this watchlist
        int isInWatchListIncludedPosition = securitiesCursor
                .getColumnIndex(DbHelper.IS_SECURITY_IN_WATCHLIST_ALIAS);
        for (int i = 0; i < securitiesCursor.getCount(); i++) {
            securitiesCursor.moveToPosition(i);
            Log.v(CLASS_NAME, String.format("%s(): securitiesCursor[%d].%s = %d",
                    methodName, i, DbHelper.IS_SECURITY_IN_WATCHLIST_ALIAS,
                    securitiesCursor.getInt(isInWatchListIncludedPosition)));
            if (securitiesCursor.getInt(isInWatchListIncludedPosition) == 1) {
                securitiesToIncludeListView.setItemChecked(i, true);
            }
        }
    } // refreshSecuritiesList()
} // class WatchlistEditActivity

WatchlistEditActivity.onCreate() checks the watchlist ID from the intent’s extras. If it’s the special one for a new watchlist it empties the Name TextView and sets the screen’s title appropriately. Finally it calls refreshSecuritiesList() which updates securitiesListView with a cursor containing all the securities from the database and marks those securities with DbHelper.IS_SECURITY_IN_WATCHLIST_ALIAS == 1 (see below). Of course the cursor cannot be closed as it has been passed over to the SimpleCursorAdapter.

Finally DbHelper’s required extensions:

public class DbHelper extends SQLiteOpenHelper {
    public final static long NEW_ITEM_ID = -1L;
    // Alias for generated columns of getAllSecuritiesAndMarkIfInWatchlist() and readAllWatchlists()
    public final static String IS_SECURITY_IN_WATCHLIST_ALIAS = "isSecurityInWatchlist";
    // ...

    /**
     * Gets a list of all securities with those in the specified watchlist marked,
     * ordered by symbol
     *
     * @param idOfWatchlistToMark
     *            If a stock is included in this watchlist,
     *            is_in_watchlist_included will be 1, otherwise 0
     * @return A list (_id, is_included_in_watchlist, symbol) of all securities
     * with those in the specified watchlist marked, ordered by symbol
     */
    public Cursor getAllSecuritiesAndMarkIfInWatchlist(long idOfWatchlistToMark) {
        final String methodName = "getAllSecuritiesAndMarkIfInWatchlist";
        Cursor cursor = null;
        Log.v(CLASS_NAME, String.format("%s(): idOfWatchlistToMark = %d",
                methodName, idOfWatchlistToMark));
        SQLiteDatabase db = this.getReadableDatabase();
        String sql = "SELECT tmp._id AS "
                + Security.ID
                + ", tmp.symbol AS "
                + Security.SYMBOL
                + ", q.name AS "
                + Quote.NAME
                + ", MAX(tmp.isInWatchList) AS "
                + IS_SECURITY_IN_WATCHLIST_ALIAS
                + "\nFROM ("
                + "\n\tSELECT " + Security.ID + ", " + Security.SYMBOL + ", 1 AS isInWatchList"
                + "\n\tFROM " + Security.TABLE + " s"
                + "\n\t\tLEFT JOIN " + SecuritiesInWatchlists.TABLE + " siwl ON "
                + SecuritiesInWatchlists.SECURITY_ID + " = " + Security.ID
                + "\n\tWHERE siwl." + SecuritiesInWatchlists.WATCHLIST_ID + " = ?"
                + "\n\tUNION ALL"
                + "\n\tSELECT " + Security.ID + ", " + Security.SYMBOL + ", 0 AS isInWatchList"
                + "\n\tFROM " + Security.TABLE + " s"
                + "\n) AS tmp"
                + "\n\tLEFT OUTER JOIN " + Quote.TABLE + " q ON q." + Quote.SECURITY_ID + " = tmp._id"
                + "\nGROUP BY tmp._id, tmp.symbol, " + Quote.NAME
                + "\nORDER BY tmp.symbol ASC";
        String[] selectionArgs = new String[] { String.valueOf(idOfWatchlistToMark) };
        logSql(methodName, sql, selectionArgs);
        cursor = db.rawQuery(sql, selectionArgs);
        Log.v(CLASS_NAME, String.format(
                CURSOR_COUNT_FORMAT, methodName, cursor.getCount()));
        return cursor;
    } // getAllSecuritiesAndMarkIfInWatchlist()

    // ...
}

This is the SQL generated by DbHelper.getAllSecuritiesAndMarkIfInWatchlist():

 SELECT tmp._id AS _id, tmp.symbol AS symbol, q.name AS name, MAX(tmp.isInWatchlist) AS isSecurityInWatchlist
 FROM (
	SELECT _id, symbol, 1 AS isInWatchlist
	FROM security s
		LEFT JOIN securities_in_watchlists siwl ON security_id = _id
	WHERE siwl.watchlist_id = -1
	UNION ALL
	SELECT _id, symbol, 0 AS isInWatchlist
	FROM security s
 ) AS tmp
	LEFT OUTER JOIN quote q ON q.security_id = tmp._id
 GROUP BY tmp._id, tmp.symbol, name
 ORDER BY tmp.symbol ASC

The SQL contains an outer and 2 inner SELECTs. The first inner SELECT lists all the securities that are in the watchlist – none in this case as this is a new watchlist – and sets isInWatchlist to 1 for them. The second inner SELECT lists all the securities no matter if they are included in any watchlist and sets isInWatchlist to 0 for them.

Both lists of securities are then combined into a new temporary list by a UNION. The temporary list will have two items for each security in the watchlist – one with isInWatchlist == 0 and one with isInWatchlist == 1. By selecting only the one with MAX(tmp.isInWatchlist) into the final list it will contain a single item for each security and those in the watchlist will have isInWatchlist == 1.

New Edit Watchlist screen

New Edit Watchlist screen

Run the app again:

  1. Open the Manage Watchlists screen
  2. Tap New:
    1. The new WatchlistEditActivity shows up listing all securities and ready to receive a name
    2. Again no menu but a distinct title
  3. Tap Cancel – tapping OK will crash the app
  4. Optional: commit changes

Note that that I closed the automatically displayed on-screen keyboard so it doesn’t obstruct the activity’s screen.

For now watchlist management is all form but no function. Time to make it operational.

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

Posted in Uncategorized | Tagged , | Leave a comment