Friday, December 29, 2017

Building an Android widget-only app to track cryptocurrency prices on the home screen

Hi folks!
After exploring apartment and single-family house prices in Vienna I realized that I will need more money than I have now. Among the several ideas to tackle this problem I came up with, one is to invest a little into the crypto world and hope for ridiculously high profits (I do not do smileys, so please imagine a winking smiley here). After some research I decided to take my chances with COSS, which returns a weekly share of all transaction fees on the crypto-exchange coss.io.
So far, so good, but now I would like a way to easily track the COSS price, ideally a home screen widget for my phone. Unfortunately, with COSS ranking in the 300’s of the largest cryptocurrencies there is no specific price widget for android and not even generic multiple coin widgets are a solution, because they mostly get their data from the major exchanges where COSS is not listed (yet). So I have to put my own price widget together, which is fortunately quite easy.
I will build a widget only app which gets the data from the coinmarketcap API.

Building a widget app in Android Studio

1. Create a new project

We start by creating a new project. I will call it com.bernhardlearns.minimalisticcosswidget. From the templates we choose an empty activity and deselect the creation of a layout file. Let’s call the activity MyWidgetConfigurationActivity which we will later use to choose between a light and a dark theme for the price widget.

2. Create all string resources

The app will not have too much text, so let’s quickly define all needed strings in res/values/strings.xml. My file looks like:
<resources>
    <string name="app_name">MinimalisticCossWidget</string>
    <string name="app_label">Minimalistic Coss Widget</string>

    <string name="message">Choose your theme</string>
    <string name="dark">dark</string>
    <string name="light">light</string>

    <string name="price">%1$s $</string>
    <string name="base_url" translatable="false">https://api.coinmarketcap.com/v1/ticker/</string>

    <string name="shared_prefs" translatable="false">mcw_shared_prefs</string>
</resources>
The string price contains a string placeholder (%1$s) which we will replace with the actual price during run-time. base_url and shared_prefs have the attribute translatable="false", because they are not displayed to the user, but are used within the code. So even if I release a translation for this app in the future, those strings will not differ.

3. Add colors for dark and light theme

I change my phone’s home screen background regularly, so I need the widget to be able to change the text color to keep it visible. To keep it simple, a light and a dark grey will suffice for now. To res/values/colors.xml we add:
<color name="dark">#424242</color>
<color name="light">#FAFAFA</color>

4. Create the widget layout

In res/layout/ we create widget_layout.xml:
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:gravity="center"
    >
    <TextView
        android:id="@+id/coin"
        style="@android:style/TextAppearance.Small"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="COSS" >
    </TextView>
    <TextView
        android:id="@+id/price"
        style="@android:style/TextAppearance.Medium"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="@string/price" >
    </TextView>

</LinearLayout>
As you can see, the layout is quite easy with only two TextViews - one to display the currency name and one for the price (hence the name minimalistic coss widget).

5. Create widget configuration activity

To allow the user to decide between light and dark we need a configuration activity (if we would not allow any user adjustments, then we could do without a configuration activity).
We modify MyWidgetConfigurationActivity.java to
package com.bernhardlearns.minimalisticcosswidget;


import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.DialogInterface;
import android.content.Intent;
import android.content.SharedPreferences;
import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;


public class MyWidgetConfigurationActivity extends AppCompatActivity {

    int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
    boolean dark = false;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setResult(RESULT_CANCELED);

        //Get theme
        startDialog();
    }

    private void startDialog() {
        AlertDialog.Builder builder = new AlertDialog.Builder(this);

        builder.setMessage(getString(R.string.message))
            .setCancelable(false)
            .setPositiveButton(getString(R.string.light), new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    dark = false;
                    applyTheme();
                }
            })
            .setNegativeButton(getString(R.string.dark), new DialogInterface.OnClickListener() {
                @Override
                public void onClick(DialogInterface dialogInterface, int i) {
                    dark = true;
                    applyTheme();
                }
            });

        builder.show();
    }

    private void applyTheme() {

        //Get widget ID
        Intent intent = getIntent();
        Bundle extras = intent.getExtras();
        if (extras != null) {
            appWidgetId = extras.getInt(
                    AppWidgetManager.EXTRA_APPWIDGET_ID,
                    AppWidgetManager.INVALID_APPWIDGET_ID);
        }
        //Store theme for widget
        SharedPreferences sharedPref = getSharedPreferences(getString(R.string.shared_prefs), Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sharedPref.edit();
        editor.putBoolean("widget" + String.valueOf(appWidgetId), dark);
        editor.commit();

        //Update widget
        AppWidgetManager widgetManager = AppWidgetManager.getInstance(this);
        MyWidgetProvider.updateWidget(getApplicationContext(), widgetManager, appWidgetId);

        //Create the return Intent,
        // set it with the Activity result,
        // and finish the Activity
        Intent resultValue = new Intent();
        resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
        setResult(RESULT_OK, resultValue);
        finish();
    }

}
There is not much to see in this activity. In onCreate() we start startDialog() which creates a simply two button dialog to prompt the user for her theme preference. The user response is then stored via applyTheme() to the shared preferences so that the widget can retrieve the user’s decision. Finally, the widget is updated for the first time, the system is informed that the configuration is ready, and the activity is finished.

6. Create AppWidgetProvider (JAVA)

To actually update the widget we need an AppWidgetProvider. We create a new Java class in our package and I name mine MyWidgetProvider, which looks like:
package com.bernhardlearns.minimalisticcosswidget;

import android.app.PendingIntent;
import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.os.StrictMode;
import android.support.v4.content.ContextCompat;
import android.util.Log;
import android.widget.RemoteViews;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;
import org.json.JSONTokener;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;

public class MyWidgetProvider extends AppWidgetProvider {

    static int[] appWidgetIds;

    @Override
    public void onUpdate(Context context, AppWidgetManager appWidgetManager,
                         int[] appWidgetIds) {
        this.appWidgetIds = appWidgetIds;
        final int N = appWidgetIds.length;
        for (int i=0; i<N; i++) {
            int appWidgetId = appWidgetIds[i];
            updateWidget(context, appWidgetManager,appWidgetId);
        }
    }

    private static void setOnClickListener(Context context, RemoteViews remoteViews) {
        Intent intent = new Intent(context, MyWidgetProvider.class);

        intent.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE);
        intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds);

        PendingIntent pendingIntent = PendingIntent.getBroadcast(context,
                0, intent, PendingIntent.FLAG_UPDATE_CURRENT);
        remoteViews.setOnClickPendingIntent(R.id.price, pendingIntent);
    }

    public static void updateWidget(Context context, AppWidgetManager appWidgetManager, int appWidgetId){
        // Get current price
        String price = getPrice("COSS", context);

        //Get theme
        SharedPreferences sharedPref = context.getSharedPreferences(context.getString(R.string.shared_prefs), Context.MODE_PRIVATE);
        boolean dark = sharedPref.getBoolean("widget" + String.valueOf(appWidgetId), true);
        int color = (dark) ? ContextCompat.getColor(context, R.color.dark) : ContextCompat.getColor(context, R.color.light);

        //Set views
        RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
                R.layout.widget_layout);
        remoteViews.setTextViewText(R.id.price, context.getString(R.string.price, price));
        remoteViews.setTextColor(R.id.price, color);
        remoteViews.setTextColor(R.id.coin, color);

        //Register an onClickListener
        setOnClickListener(context, remoteViews);

        //Update
        appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
    }

    private static String getPrice(String coin, Context context) {
        //Declare variables
        String response;
        String price = "???";
        Double db_price = null;

        //Allow networking operation in main thread
        //todo: move to async
        StrictMode.ThreadPolicy policy = new StrictMode.ThreadPolicy.Builder()
                .permitAll().build();
        StrictMode.setThreadPolicy(policy);

        //Get price from API
        String url_string = context.getString(R.string.base_url) + coin + "/";
        try {
            URL url = new URL( url_string);
            HttpURLConnection urlConnection = (HttpURLConnection) url.openConnection();
            try {
                BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(urlConnection.getInputStream()));
                StringBuilder stringBuilder = new StringBuilder();
                String line;
                while ((line = bufferedReader.readLine()) != null) {
                    stringBuilder.append(line).append("\n");
                }
                bufferedReader.close();
                response = stringBuilder.toString();
            }
            finally{
                urlConnection.disconnect();
            }
        }
        catch(Exception e) {
            Log.e("ERROR", e.getMessage(), e);
            response = "";
        }

        //Parse JSON API response and extract price_usd
        if (!response.isEmpty()) {
            try {
                JSONArray array = (JSONArray) new JSONTokener(response).nextValue();
                JSONObject object = array.getJSONObject(0);
                db_price = object.getDouble("price_usd");
            }
            catch (JSONException e){
                Log.e("ERROR", e.getMessage(), e);
            }

            price = String.format("%.3f", db_price);

        }
        return price;
    }


}
The main method of the AppWidgetProvider is onUpdate() which is called for every widget update. Because one might have more than one active minimalistic coss widget, we have to iterate through all and call our custom updateWidget() method. It sets the text and the text color of the widget. To get the price it calls the function getPrice(), which retrieves the price ticker JSON from the coinmarketcap API, extracts the double value of the price in USD and returns it as a String with three decimal places (in order to be ready for large price changes in the future and easy adaptability to other coins and tokens a more adaptive format than three decimal places would be wiser, but for now I want to keep it as simple as possible). Further, we set an OnClickListerner on the price-TextView, so that the user can request an updated and does not have to wait for the automatic update.

7. Create AppWidgetProvider (XML)

Beside the AppWidgetProvider Java class we need an additional XML to set the interval for automatic updates, register the widget layout and the configuration activity, and set a preview image. To create this XML file right click the res folder and select New -> XML -> Values XML File. I have named mine widget_info.xml and it contains
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/widget_layout"
    android:minHeight="60dp"
    android:minWidth="60dp"
    android:updatePeriodMillis="1800000"
    android:configure="com.bernhardlearns.minimalisticcosswidget.MyWidgetConfigurationActivity"
    android:previewImage="@drawable/preview">
</appwidget-provider>
If you need more control, you can use a Service instead of the provider to update your widget, but I will not do that in this post.

8. Register everything in AndroidManifest

Finally, we have to register the AppWidgetProvider and the Configuration Activity in the manifest and request permission to use the internet to enable data retrieval from the coinmarketcap API. My final AndroidManifest.xml looks like:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.bernhardlearns.minimalisticcosswidget"
    android:versionCode="1"
    android:versionName="1.0">

    <uses-permission android:name="android.permission.INTERNET" />

    <application
        android:allowBackup="true"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_label"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/AppTheme">
        <receiver android:name="com.bernhardlearns.minimalisticcosswidget.MyWidgetProvider">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
            </intent-filter>

            <meta-data
                android:name="android.appwidget.provider"
                android:resource="@xml/widget_info" />
        </receiver>

        <activity android:name="com.bernhardlearns.minimalisticcosswidget.MyWidgetConfigurationActivity">
            <intent-filter>
                <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
            </intent-filter>
        </activity>
    </application>

</manifest>

9. A few remarks

With this the app should be ready and look like this:
Screenshot of homescreen with dark and light widget.
If you want to take a look at the real product, then you can download it at Google Play Please feel free to use the code and create a better app (I am happy if you mention me, but it is not required). Some ways to do so are:
  • add a (optionally transparent) background
  • make the widget more generic to support all coins listed on coinmarketcap.com
  • allow the user to select the currency (not everybody uses USD!)
  • allow the user to display other values than the price; e.g., change in the last 24h
If you do so, then you might notice that you cannot simply run a widget only app on your (virtual) debugging device(s) by clicking the run button. Instead you have to build an apk and install it manually. For virtual devices you can do this via drag & drop of the apk-file.
Please let me know when you build a better version so that I can get it for my phone.

Closing Remarks

I hope you forgive me for the little digression of this post from the usually more data-prone theme of this blog. Please let me know, whether similar posts are of interest to you (maybe one out of five or ten) or if I should stick to the previous more data-centric theme.
Please DO NOT consider this post as an investment advice. Investing in cryptocurrencies might get you high gains, but it also poses high risks (e.g., total loss of your investment). So please do your own research. If you still like COSS after the research, consider registering at coss.io with this link - it costs you nothing, but gets me 10% of your fees in the first 30 days after registration.
If you have any questions or comments, please post them in the comments section.

4 comments:

  1. A friend just informed me that bitrift already lists COSS. Their widget is much better than my simple one, so there was no real need for a new app. I hope you enjoy my blog anyways.

    ReplyDelete
  2. What is great respecting is dealing with instead of depending on. macbook mockups

    ReplyDelete
  3. I recently came across your blog and have been reading along. I thought I could leave my first comment. I don’t know what to say except that I have enjoyed scaning what you all have to say device mockup

    ReplyDelete
  4. I like this site its a master peace ! Glad I detected this on google . android phone template

    ReplyDelete

Recommended Post

Follow the white robot - Exploring retweets of Austrian politicians with Botometer in R

botometer_publish.utf8.md Hi folks! I guess you are aware that social medi...

Popular Posts