Wednesday, 20 August 2014

Handling Preferences in an Android Application: an Example

While the main function of the Beacon Scanner & Logger app is to scan for and log beacon data, there are a number of functions common to many Android apps that I've needed to implement while putting the app together. I thought I'd document how I implemented some of this functionality in a "cookbook" style, in the hope that it proves useful to someone.

It's entirely possible I've made an omission or a mistake in the following. Please feel free to let me know if you spot anything amiss and in return I promise to bestow upon you my eternal gratitude. 

First up is handling application preferences: Implementing preferences is actually not too difficult, as the framework handles most of the heavy lifting for the developer, but there is a fair amount of configuration and a few lines of code required to get preference support up and running.

I've stripped out some of the code from the following examples, where the code removed is not directly related to handling preferences. If you'd like to view the code in its entirety, feel free to browse the app's GitHub repository.

The following steps are not presented in any particular order, but they are all mandatory.

Create a PreferenceFragment 

Since the Honeycomb release of Android, the recommended mechanism for handling preferences has been to use a Fragment rather than an Activity. Far be it from me to go against the Android teams sage advice, so I've used a Fragment in the Beacon Scanner app. The Fragment is responsible for loading up our preference definitions from the XML file we'll create in the next step, as you'll see from the code copied below:

package net.jmodwyer.beacon.beaconPoC;

import net.jmodwyer.ibeacon.ibeaconPoC.R;
import android.os.Bundle;
import android.preference.PreferenceFragment;

public class BeaconPoCPreferencesFragment extends PreferenceFragment {

    @Override
    public void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    addPreferencesFromResource(R.xml.preferences);
    }

}

Create an XML Preferences file

We'll need to create a file to hold your preference definitions. This file should live in our applications \res\xml folder, and in this example I've called the file preferences.xml, which you'll see is referenced in the PreferenceFragment implementation shown in the previous step. Here's the contents of the file for the Beacon Scanner & Logger:

<PreferenceScreen xmlns:android="http://schemas.android.com/apk/res/android" >
    
    <PreferenceCategory android:title="Beacon Properties to capture:" >

        <CheckBoxPreference
            android:defaultValue="true"
            android:key="index"
            android:summary="Log a count of beacon pings detected so far"
            android:title="Row Number" />        
        
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="uuid"
            android:summary="Log the UUID"
            android:title="UUID" />
                
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="majorMinor"
            android:summary="Log the Major and Minor values"
            android:title="Major Minor" />
        
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="rssi"
            android:summary="Log the RSSI value"
            android:title="RSSI" />
        
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="proximity"
            android:summary="Log the Proximity value"
            android:title="Proximity" />
        
        <CheckBoxPreference
            android:defaultValue="true"
            android:key="power"
            android:summary="Log the Power value"
            android:title="Power" />
                                                                        
        <CheckBoxPreference
            android:defaultValue="false"
            android:key="timestamp"
            android:summary="Log a Timestamp every time a beacon is detected"
            android:title="Timestamp" />
                                        
    </PreferenceCategory>

</PreferenceScreen>

Create an Activity so we can interact with our Preferences 

We'll need to provide an Activity that can be invoked when the user selects the Preferences option within the app. This Activity is responsible for invoking the PreferenceFragment we created earlier. Here's the code:

package net.jmodwyer.beacon.beaconPoC;

import android.app.Activity;
import android.os.Bundle;

public class AppPreferenceActivity extends Activity {

@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
  getFragmentManager().beginTransaction().replace(android.R.id.content,
               new BeaconPoCPreferencesFragment()).commit();
}

}


Add the Activity to our AndroidManifest.XML file.

Every Activity needs an entry in AndroidManifest, and this is ours:

        <activity
            android:name="net.jmodwyer.beacon.beaconPoC.AppPreferenceActivity"
            android:label="@string/app_name" >
            <intent-filter>
<action android:name="net.jmodwyer.ibeacon.ibeaconPoC.AppPreferenceActivity" />
                <category android:name="android.intent.category.DEFAULT" />
            </intent-filter>
        </activity>

Create a \res\menu\main_activity_actions.xml file

We need to create an xml file to define the options we'll display on our action bar menu.

<menu xmlns:android="http://schemas.android.com/apk/res/android">

    <item android:id="@+id/Settings"
          android:title="@string/Settings"
          android:showAsAction="never"/>

</menu>

Modify \res\values\strings.xml

We defined a string called "Settings" in the main_activity_actions.xml file, so we now need to provide a definition for this in our application's strings.xml file.

<resources>

    <string name="Settings">Settings</string>

</resources>

Update our main Activity class

We need to make a number of changes to our main Activity class that will enable preference functionality within the app. For the sake of readability I've removed code that isn't related to the handling of preferences.

First we add code to our onCreate() method to load up the default values from our preferences file:

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
PreferenceManager.setDefaultValues(this, R.xml.preferences, false);

    }

Then we ensure the options menu, which is home to our Preferences option, is displayed when our main Activity is invoked:

    @Override
    public boolean onCreateOptionsMenu(Menu menu) {
        MenuInflater inflater = getMenuInflater();
        inflater.inflate(R.menu.main_activity_actions, menu);
        return super.onCreateOptionsMenu(menu);
    }

We'll need to override the onOptionsItemSelected(MenuItem) method so we can handle the user selecting the "Settings" option, which invokes our Preferences, from the action bar.

    // Handle the user selecting "Settings" from the action bar.
    @Override
    public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
    case R.id.Settings:
        // Show settings
    Intent api = new Intent(this, AppPreferenceActivity.class);
        startActivityForResult(api, 0);
        return true;        
    default:
        return super.onOptionsItemSelected(item);
}
    }

Those are the only modifications required to load and support modification of preferences, but we obviously want to use the values store in these preferences within the application. Here's a section of the app's startScanning method, which reads preference values and stores them in Activity scoped variables for use in a subsequent method (note the use of constants for preference keys,  which are declared elsewhere in the Activity class):

    private void startScanning(Button scanButton) {

        // Get current values for logging preferences
SharedPreferences sharedPrefs =   PreferenceManager.getDefaultSharedPreferences(this);
HashMap <String, Object> prefs = new HashMap<String, Object>();
prefs.putAll(sharedPrefs.getAll());
   
index = (Boolean)prefs.get(PREFERENCE_INDEX);
uuid = (Boolean)prefs.get(PREFERENCE_UUID);
majorMinor = (Boolean)prefs.get(PREFERENCE_MAJORMINOR);
rssi = (Boolean)prefs.get(PREFERENCE_RSSI); 
proximity = (Boolean)prefs.get(PREFERENCE_PROXIMITY);
power = (Boolean)prefs.get(PREFERENCE_POWER);
timestamp = (Boolean)prefs.get(PREFERENCE_TIMESTAMP);
    }


...And we're done. If we follow the above steps we should now have an application in which we can display, modify and retrieve preferences. If you're still with me then thanks for your time, I hope this has helped you, and I'd love to have your feedback.

Post a Comment