Wednesday, June 6, 2012

PhoneGap Android Application Preferences Plugin

So I'm working on a side app that I get to touch about once a month for about 15 minutes and one of the things I ran into is that I need a way to store user preferences. Well I realized that I could build my own HTML page and store the data in localStorage but I wouldn't learn anything new that way. In the end I decided to do a plugin that would allow PhoneGap developers to use Android Application Preferences and I modelled it after the existing iOS plugin by originally by Tue Topholm (Randy McMillian ported it to the updated Plugin API on iOS).

First if you are unsure about how application preferences are setup on Android go read this tutorial and then come back I'll wait. Oh great, your back. Let's crack on with things.

Functionality

Okay, so the application preferences plugin will provide you with four methods that you can use to interact with the native Android preferences.

get(key, success, fail)

If the key exists in the preferences it will be returned as the single parameter of the success callback that you provide. If the key does not exist the failure callback will be executed with an error object with error.code set to 0 which means no property.

    window.plugins.applicationPreferences.get(key, function(value) {
        console.log("The value is = " + value);
    }, function(error) {
        console.log(JSON.stringify(error));
    });

set(key, value, success, fail)

If the key exists in the preferences then value will be saved and your the success callback will be executed. If the key does not exist the failure callback will be executed with an error object with error.code set to 0 which again means no property.

    window.plugins.applicationPreferences.set(key, value, function(value) {
        console.log("set correctly");
    }, function(error) {
        console.log(JSON.stringify(error));
    });



load(success, fail)

Calling load will have the native side loop through all the preferences creating a JSON object that will be returned as the single parameter of your success callback.

    window.plugins.applicationPreferences.load(function(value) {
        console.log("The object is = " + JSON.stringify(value));
    }, function(error) {
        console.log(JSON.stringify(error));
    });


show(activity, success, fail)

Calling show passing in the class name of your PreferenceActivity class will cause the native Android GUI to be shown so your user can interact with the preferences. If the class name you pass in doesn't exists your failure callback will be called with an error object with error.code set to 1 which means no preferences activity.

    window.plugins.applicationPreferences.show("com.simonmacdonald.prefs.QuickPrefsActivity");

which brings up a GUI that looks like this:


Installation

Installation of the Plugin follows along the common steps:

  1. Add the script tag to your html:
    <script type="text/javascript" charset="utf-8" src="applicationPreferences.js"/>
  2. Copy the Java code into your project to the src/com/simonmacdonald/prefs folder.
  3. Create a preferences file named res/xml/preferences.xml following the Android specification.  
  4. Finally you'll need to create a class that extends PreferenceActivity in order to be able to view/modify the preferences using the native GUI. Refer back to the tutorial I mentioned for more details. 
That's about it. Give the plugin a try if you need to store native preferences and let me know what you think.

Oh, and I'm pretty sure that Darren McEntee has already included this plugin in his Live Football on TV app which means this plugin is already in the wild.

Enjoy!

50 comments:

jax said...

Nice! Again: thanks. :)

Tue Topholm said...

It wasn't Randy that made it

Simon MacDonald said...

@Tue

Sorry for the incorrect attribution. I looked at the iOS directory when I should have been looking at the iPhone directory. I just fixed up the post to give you credit.

Rafa said...

Hi,
I'm trying to install this plugin, but I can't run it because of 1 error:

"The method startActivity(Intent) is undefined for the type CordovaInterface"

There are also several warnings:

"The value of the field AppPreferences.LOG_TAG is not used"

The method getContext() from the type CordovaInterface is deprecated

"Map is a raw type. References to generic type Map should be parameterized"

"Iterator is a raw type. References to generic type Iterator should be parameterized"

I'm using Eclipse 4.2 on Fedora

Am I missing anything?
Thanks in advance,
Rafael

Simon MacDonald said...

@Rafa

Upgrade to 2.0.0 and the error should go away. Don't worry about the warnings though.

Udo said...

Hi, Simon,

I can´t get started this AppPrefs plugin. I changed to cordova 2.0, set all things like the readme, create a preferences.xml and a QuickPrefsActivity like

package my.package;

import android.content.Intent;
import android.os.Bundle;
import android.preference.PreferenceActivity;


public class QuickPrefsActivity extends PreferenceActivity {

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

Eclipse don´t show me any error, but the emulator shows me nothing, too :(

Is there a working example?

Sorry for bad english

Udo said...

Hi, Simon ... it´s running ;) anyway, thanks

Rafa said...

Hi Simon,
First of all, and knowing that I'm very belated in saying it, thanks a lot for your previous response. Because of several issues (including having to change my OS), I couldn't check it until a few minutes ago.

The error got fixed as you said. But now I'm having another error, this time related with de javascript code. The error is
Result of expression 'window.plugins' [undefined] is not an object at file ...js:196
The referred line is:
function dime_pref(key,defecto) {
window.plugins.applicationPreferences.get(key, function(value) {
console.log("El valor de la preferencia es = " + value);
return value;
}, function(error) {
console.log(JSON.stringify(error));
return defecto
});
}

Of course, your code (cordova.define(...) is previous to that line.

Once again, thank you very much in advance.

Simon MacDonald said...

@Rafa

Someone did a pull request on my plugin. You should just be able to call appPreferences.get() now. No need for the window.plugins bit.

Rafa said...

Simon, thanks for your quick response.

I must be doing something wrong. Now I get the error "ReferenceError: Can't find variable: appPreferences"

I'm not much experienced on javascript, so I'm not sure how this works, but I notice that appPreferences is defined inside (more precisely, at the end of them) the following lines:
cordova.define("cordova/plugin/applicationpreferences", function(require, exports, module) {
...
...
var appPreferences = new AppPreferences();
module.exports = appPreferences;
});


Am I missing anything?
Thanks again

Simon MacDonald said...

@Rafa

Sorry, I made a mistake. here is the kinda code you want to add to your deviceready handler so you don't need to modify the rest of your html.


if (!window.plugins) {
window.plugins = {};
}
if (!window.plugins.applicationPreferences) {
window.plugins.applicationPreferences = cordova.require("cordova/plugin/applicationpreferences");
}

Rafa said...

It worked! Great!

Thanks a lot.

52AMANTES said...

Using 2.0.0, I put the next code in the ondebiceReady event:

if (!window.plugins) {
window.plugins = {};
}
if (!window.plugins.applicationPreferences) {
window.plugins.applicationPreferences = cordova.require("cordova/plugin/applicationpreferences");
}


applicationPreferences.get("enterTimes", function(value) {
console.log("The value is = " + value);
}, function(error) {
console.log(JSON.stringify(error));
});

And i got the error:
module cordova/plugin/applicationpreferences not found at undefined:0

Could you help me please??

Thank you very much!! :D

Simon MacDonald said...

@52AMANTES

Make sure you are using the 2.0.0 applicationPreferences.js file and you are including it as one of your script tags.

Rafa said...

Hi, Simon,
Here I'm again

I don't know if I declared victory too soon, or made something wrong afterwards...

I have this function:
function dime_pref(key,defecto) {
var valor = ''
window.plugins.applicationPreferences.get(key, function(value) {
console.log("El valor de la preferencia "+ key + " es = " + value);
valor = value;
}, function(error) {
console.log(JSON.stringify(error));
valor = defecto;
});

return valor
}


I call it in this way:
var avance = dime_pref('avance','100')

I see that it seems to work properly because the console log shows the proper value. But that value never gets the variable "avance". What may be wrong in what I'm doing?

I can add that I'm getting also a previous error in console.log:
TypeError: Result of expression 'cordova.exec' [undefined] is not a function. And it points at the line:
cordova.exec(success,fail,"applicationPreferences","get",[key]);

As always, thank you very much.

Simon MacDonald said...

@Rafa

You are trying to call an asynchronous method in a synchronous way. What is happening is that the get method does not execute before you return valor. You'll need to set the value of avance in the success method of the get.

zaherrrr said...

Hi Simon,
Thanks for your great information regarding the app prefs.
i want to open the application preferences from the ios app. i need more info about how to implement this method,
the last phongap plugin is not updated with the methods of show and load,
i only can use the set/get methods.
can you please tell me additional information?

thanks

Simon MacDonald said...

@zaherrrr

Sorry I did not write the iOS plugin. When I was doing the Android version show/load seemed like good additions. You should add them to the iOS plugin and do a pull request.

Jack Smith said...

You give very useful information iphone android application with that useful function. it is very good stuff but

I have this function:
function dime_pref(key,defecto) {
var valor = ''
window.plugins.applicationPreferences.get(key, function(value) {
console.log("El valor de la preferencia "+ key + " es = " + value);
valor = value;
}, function(error) {
console.log(JSON.stringify(error));
valor = defecto;
});

Simon MacDonald said...

@Jack Smith

What is the question?

Rafa said...

Simon, I've been traveling out of town and so far I have not been able to get back in front of the computer, so I don't remember well if I told you that I needed to get preferences values synchronously, because I would use them the page is being constructed (font size, background-color,...)

How should I do it?
Thank you very much in advance.

Simon MacDonald said...

@Rafa

Sorry you are going to need to restructure your code to work with the async call. If you need these values for page construction you may want to:

1) Show a splash screen
2) wait for device ready
3) make a call to get the preferences
4) refresh the page with the preference values
5) hide the splash screen

Daniel Caymo said...

Im new with phonegap and plugin and I try to use the appPreferences plugin but it keep giving me error: Class Not found.
Im new with phonegap and plugin and I try to use the appPreferences plugin but it keep giving me error: Class Not found

I even put the in the config.xml folder :S so I don't know what I'm missing to make it work

Simon MacDonald said...

@Daniel Caymo

Hey, blogger won't accept xml in the comment so if you could post your manifest.xml and config.xml to a site like pastebin or gist then link back to it I could probably help.

Daniel Caymo said...

this is the link: http://pastebin.com/Ks2vyUzD
http://pastebin.com/6uJnTpjz
Thx for replying and sorry If i send a lot of msg in the sametime because I don't know if I send it D: srry I'm really new to asking people in blog for help

Simon MacDonald said...

@Daniel Caymo

The plugin line in your config.xml is wrong. The value of the plugin line should be:

"com.simonmacdonald.prefs.AppPreferences"

Daniel Caymo said...

i try that alreadybut it still wouldnt.work ;S that why i change it n i thought it wouldnt matter as long it the path where the.file.is.located

Simon MacDonald said...

@Daniel Caymo

Oh, it matters. The plugin line, the location of the file in the src folder and the package line in the the Java class must all match.

Daniel Caymo said...

ah ok I try changing it tomorrow and see what happen thx you very for fast reply i let u know tomorrow if there any.change

Daniel Caymo said...

I try to make the changes you told me and it still doesn't work and anw this is the whole application it a small one hope you be able to tell me what I'm doing wrong https://github.com/dandan28/test/down

Daniel Caymo said...

This is my last question when i compile with eclipse through a virtual machine android the plugin works but when I try compiling it with https://build.phonegap.com/
builds the plugin doesn't work

Simon MacDonald said...

@Daniel Caymo

Yeah, that is kinda essential information. You realize that PhoneGap Build does not support a ton of plugins and it does not support the AppPreferences plugin.

Daniel Caymo said...

Hi,sorry I have another question again, when I try loading the ShowPreferenceAll automatically on body onload"ShowPreferenceAll();" it doesn't work?? because I want to show my Application preferences when my app load right away

Simon MacDonald said...

@Daniel Caymo

That's because you are not waiting for the deviceready event. You won't be able to call and PhoneGap plugin or core API until after that event is fired. So listen for the event then show preferences.

Daniel Caymo said...

Thx Simon it work now :D Thx for all those fast reply :D

Jake Monton said...

Hi Simon,

I am using the 2.2.0 version of your application Preference plugin on Cordova 2.2 version but unfortunately I wasn't able to make it work. Though I don't get any error, I can't go through the success and error callback function after calling "get" method. Do I miss something on this? Thanks.

Simon MacDonald said...

@Jake Monton

Probably. What do you see in "adb logcat"?

ramonbln said...

Hey Simon,

how to get rid of the second application icon, which appears when using that plugin ?

BR
Ray

Simon MacDonald said...

@ramonbln

You should not see two application icons when you use this plugin. I suspect that you have two LAUNCHER items in your AndroidManifest.xml.

ramonbln said...

Thanks! After removing the second LAUNCHER-entry, which I copied from your tutorial, the second LauncherIcon is gone :)

BR
Ray

Edwardo Hanna said...

Hi Simon,

In my previous posts I had sent you links to codebin pastes that seem to be unreliable. As I mentioned before, thanks for your great preference plugin. It is absolutely helpful in my project. So far I have been able to load the preference activity. But when I try the load function I get the error that it is undefined. I was hoping that you would be able to help me correct this. I am uncertain as to how to reference the preference file if that is the problem. I have included links to pastes on pastebin this time hoping that they are more reliable for your viewing. These are the links:

This is the applicationPreference script:
http://pastebin.com/v8JM6HUP

This is the HTML file:
http://pastebin.com/2RFVZA57

This is the error log after succesfully seen the preferences:
http://pastebin.com/DCeAsgYE

This is the undefined message for the load function:
http://pastebin.com/MCrRC12H

Thanks for your assistance.

Best Regards,

Edward Hanna
hanna.edwardo@gmail.com

Simon MacDonald said...

@Edwardo Hanna

It is a class cast exception where an Integer is being cast as a String. Try declaring one of your numbers as a string in preferences and it should be okay.

Unknown said...

Hi Simon,

I got "undefined" result after executing "cordova.exec" at my plugin's javascript file (at "resultTemp" variable).
here is my js file:

(function(cordova){
var DeviceInfo = function() {
};

DeviceInfo.prototype.imeiNumber = function(params, success, fail) {
var resultTemp;

alert("executing ImeiNumber");
resultTemp = cordova.exec(function(args) {
success(args);
}, function(args) {
fail(args);
}, 'DeviceInfo', 'imeiNumber', [params]);

alert(resultTemp);

return resultTemp;
};

DeviceInfo.prototype.phoneNumber = function(params, success, fail) {
return cordova.exec(function(args) {
success(args);
}, function(args) {
fail(args);
}, 'DeviceInfo', 'phoneNumber', [params]);
};

DeviceInfo.prototype.imsi = function(success, fail) {
return cordova.exec(function(args) {
success(args);
}, function(args) {
fail(args);
}, 'DeviceInfo', 'imsi', []);
};

window.deviceInfo = new DeviceInfo();

// backwards compatibility
window.plugins = window.plugins || {};
window.plugins.deviceInfo = window.deviceInfo;

})(window.PhoneGap || window.Cordova || window.cordova);

Here is my Java plugin:

import org.apache.cordova.CallbackContext;
import org.apache.cordova.CordovaPlugin;
import org.apache.cordova.PluginResult;
import org.json.JSONArray;
import org.json.JSONException;

public class DeviceInfo extends CordovaPlugin
{
public DeviceInfo()
{

}

@Override
public boolean execute(String action, JSONArray args, CallbackContext callbackContext) throws JSONException
{
PluginResult.Status status = PluginResult.Status.ERROR;
String result = "";

if (action.equals("imeiNumber"))
{
status = PluginResult.Status.OK;
result = this.DeviceImeiNumber();
}

callbackContext.sendPluginResult(new PluginResult(status, result));
return true;
}

public String DeviceImeiNumber(){
//TelephonyManager tManager = (TelephonyManager)cordova.getActivity().getSystemService(Context.TELEPHONY_SERVICE);
//return tManager.getDeviceId();
return "Success1";
}
}

and this in my html page, I use this js code to call my plugin:

function onLoad()
{
alert("I've been loaded");
document.addEventListener("deviceready", onDeviceReady, true);
}

function onDeviceReady()
{
window.plugins.deviceInfo.imeiNumber(function(imei) {
if(imei !== "")
{
alert("success, IMEI: " + imei);
}
else
{
alert("Failed :(");
}
});

Am I doing something wrong?

Many thanks before :)

Simon MacDonald said...

@Unknown

Yeah, if you are using a later version of PhoneGap you need to do a require in order to pull in the exec module. Check out:

https://git-wip-us.apache.org/repos/asf?p=cordova-plugin-camera.git;a=blob;f=www/Camera.js;h=b2da5da95bfe5755cab4ef1029745467ae9140d5;hb=HEAD

for an example. Also, don't expect cordova.exec to return you anything useful. Use the success and failure callbacks.

David Silveria said...

I might be a bit thick, but where do we create the class that extends PreferenceActivity from step #4. I'm using Icenium if that makes a difference. I though the purpose of a plugin is to save you from writing native code. Your .java file already contains the native code, so where do I extend PreferenceActivity?

Simon MacDonald said...

@David Silveria

Follow the link earlier in the blog post "go read this tutorial" to learn how to setup the Java side.

Classified said...

are there any plans to get this working with the cloud build system ?

David Silveria said...

I did read the article, but the part I do not get is where to place that code, it is never said in plain text in the article. I can only use native code in the plugins folder, which I thought already contains all needed code for the plugin.

Simon MacDonald said...

@Classified

I don't have time to work on it right now but I will gladly take pull request to my repo that provide that support. Someday I will have time to revisit all of my plugins.

Simon MacDonald said...

@David Silveria

It is up to you where you put the code. It all depends on what the package name of your app is. For instance if I create the class SimonsPrefs and it is in the package com.simonmacdonald I should put the code in src/com/simonmacdonald/SimonsPrefs.java