I must be missing something here. Every example I've seen with Android's 2-way binding is based on a String in the backing data for anything user-enterable, like an EditText.
Handling anything not a String seems somewhat... inelegant. For example, if I have a double in my domain model that needs to be editable, the best binding I've come up with requires a ViewModel with surprisingly a lot of code to interface between the model and the EditText.
Am I missing something key? Should I really need 30 lines of code to interface an EditText with a double? For the sake of discussion, let's consider a currency field, represented as a double, in a two-way bound EditText:
<EditText
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:inputType="numberDecimal"
android:text="#={fragModel.startBucks}"
android:id="#+id/editText2"/>
And here's the ViewModel I've had to construct in order to give the EditText a string to bind to.
#Bindable private String startBucksString;
private double localBucks;
public String getStartBucksString() {
double domainBucks = cd.getStartBucks();
// Ignore incoming change of less than rounding error
if( Math.abs(localBucks - domainBucks) < .001 ) return startBucksString;
startBucksString = "";
if( domainBucks != 0)
startBucksString = String.format("$%.2f", domainBucks);
return startBucksString;
}
public void setStartBucksString(String inBuckstr) {
double calcBucks=0;
inBuckstr = inBuckstr.replaceAll( "[^\\d.]", "" );
try {
calcBucks = Double.parseDouble(inBuckstr);
} catch( NumberFormatException e) {
return;
}
// Neglect outgoing change of less than rounding error
if( Math.abs(localBucks - calcBucks) < .001 ) return;
startBucksString = String.format("$%.2f", calcBucks);
localBucks = calcBucks;
cd.setStartBucks(calcBucks);
notifyPropertyChanged(BR.startBucksString);
}
Here, I wrote a simple, compilable example of 2-way binding with a ViewModel. It illustrates the difficulty I had in continuously updating a float in the domain model -- in the end, I decided there's no way to do it without writing a custom TextWatcher for each domain field.
My approach is to delay notifyPropertyChanged method calling using Handles. In this way while the user is typing, the code don't run, then 2,5 seconds after the user has stopped to type last character, the notificationPropertyChanged will be called.
The visual effect is cool, and the user is free to write numbers as he wants.
See these two examples:
Use can use this compact(?) code for each field:
//
// g1FuelCostYear field
//
private double g1FuelCostYear;
#Bindable
public String getG1FuelCostYear() {
return Double.valueOf(g1FuelCostYear).toString();
}
private Handler hG1FuelCostYearDelay = null;
public void setG1FuelCostYear(String g1FuelCostYear) {
// Delayed notification hadler creation
if (hG1FuelCostYearDelay == null) {
hG1FuelCostYearDelay = new Handler() {
#Override
public void handleMessage(Message msg) {
notifyPropertyChanged(it.techgest.airetcc2.BR.g1FuelCostYear);
}
};
} else {
// For each call remove pending notifications
hG1FuelCostYearDelay.removeCallbacksAndMessages(null);
}
// Data conversion logic
try {
this.g1FuelCostYear = Double.parseDouble(g1FuelCostYear);
}
catch (Exception ex) {
this.g1FuelCostYear = 0.0;
log(ex);
}
// New delayed field notification (other old notifications are removed before)
hG1FuelCostYearDelay.sendEmptyMessageDelayed(0,2500);
}
This code instead is useful when you use currency converter or percent converter. The user can write a plain double, the code convert to currency string. If the setter is called with the currency string the code is able to convert it as double too.
//
// g1FuelCostYear field
//
private double g1FuelCostYear;
#Bindable
public String getG1FuelCostYear() {
NumberFormat nf = NumberFormat.getCurrencyInstance();
return nf.format(this.g1FuelCostYear);
//return Double.valueOf(g1FuelCostYear).toString();
}
private Handler hG1FuelCostYearDelay = null;
public void setG1FuelCostYear(String g1FuelCostYear) {
if (hG1FuelCostYearDelay == null)
{
hG1FuelCostYearDelay = new Handler() {
#Override
public void handleMessage(Message msg) {
notifyPropertyChanged(it.techgest.airetcc2.BR.g1FuelCostYear);
}
};
} else {
hG1FuelCostYearDelay.removeCallbacksAndMessages(null);
}
boolean success = false;
try {
NumberFormat nf = NumberFormat.getCurrencyInstance();
this.g1FuelCostYear = nf.parse(g1FuelCostYear).doubleValue();
success = true;
}
catch (Exception ex) {
this.g1FuelCostYear = 0.0;
log(ex);
}
if (!success) {
try {
this.g1FuelCostYear = Double.parseDouble(g1FuelCostYear);
success = true;
} catch (Exception ex) {
this.g1FuelCostYear = 0.0;
log(ex);
}
}
updateG1FuelConsumption();
hG1FuelCostYearDelay.sendEmptyMessageDelayed(0,2500);
}
Related
I have a wearable app. The app after it finishes has data like time/date, UUID, Geo location, parameters selected displayed in front of me like a Data Report or Log in several TextViews underneath each other. Like a list. I want this data to be transferred from my wearable device to my android phone.
Now I have to ask does the WearOS app the pairs the phone with the watch enables such a thing? Like can the data be sent through it? OR what exactly can I do? I read about Sync data items with the Data Layer API in the documentation, but I'm not sure if the code snippets provided would help achieve what I want.
public class MainActivity extends Activity {
private static final String COUNT_KEY = "com.example.key.count";
private DataClient dataClient;
private int count = 0;
...
// Create a data map and put data in it
private void increaseCounter() {
PutDataMapRequest putDataMapReq = PutDataMapRequest.create("/count");
putDataMapReq.getDataMap().putInt(COUNT_KEY, count++);
PutDataRequest putDataReq = putDataMapReq.asPutDataRequest();
Task<DataItem> putDataTask = dataClient.putDataItem(putDataReq);
}
...
}
The data I display in the textviews are called through methods that I call things like: getLocation, getUUID, getDateTime, getSelections, etc... when I click a button I call them in the setOnClickListener. I want this data in the TextViews to be placed in a file or something like that and send them over to the mobile phone from the watch when they're generated.
private void getDateTime()
{
SimpleDateFormat sdf_date = new SimpleDateFormat("dd/MM/yyyy");
SimpleDateFormat sdf_time = new SimpleDateFormat("HH:mm:ss z");
String currentDate= sdf_date.format(new Date());
String currentTime= sdf_time.format(new Date());
textView_date_time.setText("Date: "+currentDate+"\n"+"Time: "+currentTime);
}
#SuppressLint("SetTextI18n")
private void getUUID()
{
// Retrieving the value using its keys the file name
// must be same in both saving and retrieving the data
#SuppressLint("WrongConstant") SharedPreferences sh = getSharedPreferences("UUID_File", MODE_APPEND);
// The value will be default as empty string because for
// the very first time when the app is opened, there is nothing to show
String theUUID = sh.getString(PREF_UNIQUE_ID, uniqueID);
// We can then use the data
textView_UUID.setText("UUID: "+theUUID);
}
#SuppressLint("SetTextI18n")
private void getSelections()
{
textView_data_selected.setText("Tool No.: "+c.getToolNo()+
"\nTool Size: " +c.getToolSizeStr()+
"\nFrom Mode: " +c.getCurrentModeStr()+
"\nGoto Mode: " +c.getModeStr()+
"\nMethod: " +c.getMethodStr()+
"\nBit Duration: " +c.getBitDuration()+
"\nUpper bound" +c.getUpStageValue()+
"\nLower bound: "+c.getDownStageValue());
}
The above are examples of the methods I use to get the data. then I call them here:
gps_btn.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (Build.VERSION.SDK_INT >= 26) {
getLocation();
getDateTime();
getUUID();
getSelections();
}
else
{
//ActivityCompat.requestPermissions(get_location.this, new String[]{Manifest.permission.ACCESS_FINE_LOCATION}, 1);
Toast.makeText(get_location.this,"Build SDK too low",Toast.LENGTH_SHORT);
}
}
});
Now how do I take all this and send it over from my device to the the phone?
Note: The data report I want to send as a file, I want it done subtly like something done in the background. I don't know what else to do or where to look.
You have two options if you want to use the Data Layer, one is to use the MessageClient API to bundle your data up in a message and send it directly to the handheld. The easiest here would be to create an arbitrary JSONObject and serialize your data as a JSON string you can stuff into a message. For example:
try {
final JSONObject object = new JSONObject();
object.put("heart_rate", (int) event.values[0]);
object.put("timestamp", Instant.now().toString());
new MessageSender("/MessageChannel", object.toString(), getApplicationContext()).start();
} catch (JSONException e) {
Log.e(TAG, "Failed to create JSON object");
}
In my case, I do this in my onSensorChanged implementation, but you can insert this wherever you are updating your text.
MessageSender is just a threaded wrapper around the MessageClient:
import java.util.List;
class MessageSender extends Thread {
private static final String TAG = "MessageSender";
String path;
String message;
Context context;
MessageSender(String path, String message, Context context) {
this.path = path;
this.message = message;
this.context = context;
}
public void run() {
try {
Task<List<Node>> nodeListTask = Wearable.getNodeClient(context.getApplicationContext()).getConnectedNodes();
List<Node> nodes = Tasks.await(nodeListTask);
byte[] payload = message.getBytes();
for (Node node : nodes) {
String nodeId = node.getId();
Task<Integer> sendMessageTask = Wearable.getMessageClient(context).sendMessage(nodeId, this.path, payload);
try {
Tasks.await(sendMessageTask);
} catch (Exception exception) {
// TODO: Implement exception handling
Log.e(TAG, "Exception thrown");
}
}
} catch (Exception exception) {
Log.e(TAG, exception.getMessage());
}
}
}
The other option is to create a nested hierarchy of data items in the Data Layer and implement DataClient.OnDataChangedListener on both sides, such that changes that are written in on one side are automatically synchronized with the other. You can find a good walkthrough on how to do that here.
For your specific case, just packing it in a JSON object would probably be the simplest. The writing out to your preferred file format you can then implement on the handheld side without needing to involve the wear side.
Title of this question might be incorrect as I am assuming this is the problem. I am new to both Java and Android and have been struggling on this for days.
I am trying to connect to an ECU (non OBD-2) via an FTDI cable, usb-to-go and an Android device.
I have based most of this code on the examples shown on the FTDI website using their Android D2xx.jar library, example code and by reading their documentation.
The problem I am experiencing is that the TextView "dataView" is not being updated at all. In Fact the following line shows an error of : "The value of the local variable dataView is not used" - The TextView append line is not being seen due to it being in a separate thread? Or this possibly indicates to me that my implementation of a message handler could be incorrect or something similar?
In addition to this, the data from the ECU is expected in hex format , 6 bytes to be precise initially. So the second part of my question is is how to display the hex info in textView as hex in string format?
TextView dataView = (TextView) findViewById(R.id.dataView);
-----------------------------------------------------------
final Handler handler = new Handler() {
#Override
public void handleMessage(Message msg) {
if (iavailable > 0) {
dataView.append(String.copyValueOf(readDataToText, 0, iavailable));
}
}
};
------------------------------------------------------------
public class readECUThread extends Thread {
Handler mHandler;
readECUThread(Handler h) {
mHandler = h;
this.setPriority(Thread.MIN_PRIORITY);
}
#Override
public void run() {
int i;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
}
synchronized (ftDev) {
iavailable = ftDev.getQueueStatus();
if (iavailable > 0) {
if (iavailable > readLength) {
iavailable = readLength;
}
ftDev.read(readData, iavailable);
for (i = 0; i < iavailable; i++) {
readDataToText[i] = (char) readData[i];
}
Message msg = mHandler.obtainMessage();
mHandler.sendMessage(msg);
}
}
}
}
This was a simple stupid error on my part in the 1st line of my originally posted code:
TextView dataView = (TextView) findViewById(R.id.dataView);
Should have just been :
dataView = (TextView) findViewById(R.id.dataView);
Eclipse did not pick this up as an error so it had me scratching my head for a while. I now get data to the TextView field but in very strange characters (possibly Korean as it is a Samsung phone?).
I was working on capturing the order of elements contained in tag. Here is all the code:
League.java:
#Root
#Convert(value = LeagueConverter.class)
public class League
{
#Attribute
private String name;
#Element(name="headlines", required = false)
private Headlines headlines;
#Element(name="scores", required = false)
private Scores scores;
#Element(name="standings", required = false)
private Standing standings;
#Element(name="statistics", required = false)
private LeagueStatistics statistics;
public List<String> order = new ArrayList<String>();
// get methods for all variables
}
LeagueConverter.java:
public class LeagueConverter implements Converter<League>
{
#Override
public League read(InputNode node) throws Exception
{
League league = new League();
InputNode next = node.getNext();
while( next != null )
{
String tag = next.getName();
if(tag.equalsIgnoreCase("headlines"))
{
league.order.add("headlines");
}
else if(tag.equalsIgnoreCase("scores"))
{
league.order.add("scores");
}
else if(tag.equalsIgnoreCase("statistics"))
{
league.order.add("statistics");
}
else if(tag.equalsIgnoreCase("standings"))
{
league.order.add("standings");
}
next = node.getNext();
}
return league;
}
#Override
public void write(OutputNode arg0, League arg1) throws Exception
{
throw new UnsupportedOperationException("Not supported yet.");
}
}
Exampe of XML:
<android>
<leagues>
<league name ="A">
<Headlines></Headlines>
<Scores></Scores>
...
</league>
<league name ="B">...</league>
</leagues>
</android>
How I'm calling it and expecting it to behave: (Snippet)
Android android = null;
Serializer serial = new Persister(new AnnotationStrategy());
android = serial.read(Android.class, source);
Log.i("Number of leagues found ",tsnAndroid.getLeagueCount() + ""); // prints fine
League nhl = tsnAndroid.getLeagues().get(0); // works fine
// DOES NOT WORK throws NullPointerEx
League nhl2 = tsnAndroid.getLeagueByName("A");
// DOES NOT WORK throws NullPointerEx
for(String s : nhl.getOrder())
{
Log.i("ORDER>>>>>", s);
}
The problem:
android.getLeagueByName() (Works with #Attribute name) suddenly stops working when I have the converter set, so its like the following from League.java, never gets set.
#Attribute
private String name; // not being set
However, when I comment out the converter declaration in League.java - Every league has an attribute called name and android.getLeagueByName() starts working fine...
Does #Convert for League somehow interfere with #Attribute in League?
Even though this question is outrageously old (as is the SimpleXML library), I will give my two cents.
#Convert annotation works only with #Element, but it does not have any effect on #Attribute. I'm not sure if that's a bug or a feature, but there is another way of handling custom serialized objects - called Transform with Matcher, and it works both with Attributes and with Elements. Instead of using the Converters, you define a Transform class that handles serialization and deserialization:
import java.util.UUID;
import org.simpleframework.xml.transform.Transform;
public class UUIDTransform implements Transform<UUID> {
#Override
public UUID read(String value) throws Exception {
return value != null ? UUID.fromString(value) : null;
}
#Override
public String write(UUID value) throws Exception {
return value != null ? value.toString() : null;
}
}
As you can see, it is more straight-forward than implementing the Convert interface!
Create a similar class for all your objects that require custom de/serialization.
Now instantiate a RegistryMatcher object and register there your custom classes with their corresponding Transform classes. This is a thread-safe object that internally uses a cache, so it might be a good idea to keep it as a singleton.
private static final RegistryMatcher REGISTRY_MATCHER = new RegistryMatcher();
static {
try {
REGISTRY_MATCHER.bind(UUID.class, UUIDTransform.class);
// register all your Transform classes here...
} catch (Exception e) {
throw new RuntimeException(e);
}
}
Finally, you can create a Persister class each time before a conversion and pass it the AnnotationStrategy together with your RegistryMatcher instance. In this factory method below, we will also use an indenting formatter:
private static Persister createPersister(int indent) {
return new Persister(new AnnotationStrategy(), REGISTRY_MATCHER, new Format(indent));
}
Now you can make your serialization/deserialization methods:
public static String objectToXml(Object object, int indent) throws MyObjectConversionException {
ByteArrayOutputStream out = new ByteArrayOutputStream();
Persister p = createPersister(indent);
try {
p.write(object, out, "UTF-8");
return out.toString("UTF-8");
} catch (Exception e) {
throw new MyObjectConversionException("Cannot serialize object " + object + " to XML: " + e.getMessage(), e);
}
}
public static <T> T xmlToObject(String xml, final Class<T> clazz) throws MyObjectConversionException {
Persister p = createPersister(0);
try {
return (T) p.read(clazz, xml);
} catch (Exception e) {
throw new MyObjectConversionException(
"Cannot deserialize XML to object of type " + clazz + ": " + e.getMessage(), e);
}
}
The only issue with this approach is when you want to have different formatting for the same object - e.g. once you want the java.util.Date to have just the date component, while later on you also want to have the time component. Then just extend the Date class, calling it DateWithTime, and make a different Transform for it.
#ElementListUnion will capture the order of elements
The #Convert annotation works only on #Element fields. I am struggling against converting #Attribute fields too but with no success for now...
I am making a game using Andengine/Box2D physics addon. I experienced the random crashes due to the addition/movement/deletion of box2d bodies during the world step calculation, so I have implemented code to flag sprites/bodies for removal using the setUserData method - I attach a JSON object to each body and sprite that contains the type of sprite, the body, and the sprite itself and its delete status:
private JSONObject makeUserData(int type, Body body, Object sprite)
{
JSONObject myObject = new JSONObject();
try {
myObject.put("type", type);
myObject.put("sprite", sprite);
myObject.put("body", body);
myObject.put("deleteStatus", false);
} catch (JSONException e) {
Log.d(TAG,"Exception creating user data:"+e);
}
return myObject;
}
then in an update thread iterate through all the bodies in my world looking for these flags and delete the sprites/bodies with the flag. The bodies remove correctly, however the sprite removal seems to delete every instance of that particluar sprite rather than just removing the particular one i flagged to remove! I can tell the bodies are still present without the sprite as my player collides with invisible objects! Here is the code for removal:
private void removeObjectsSetForDestruction()
{
if(this.mPhysicsWorld!=null)
{
Iterator<Body> allMyBodies = this.mPhysicsWorld.getBodies();
boolean isDelete = false;
JSONObject currentBodyData;
while(allMyBodies.hasNext())
{
try {
currentBodyData = (JSONObject)allMyBodies.next().getUserData();
if(currentBodyData!=null)
{
isDelete = (Boolean) currentBodyData.get("deleteStatus");
if(isDelete)
{
destroyObstruction((Body) currentBodyData.get("body"));
}
}
} catch (JSONException e) {
Log.d(TAG,"Error getting world bodies data:"+e);
}
}
}
}
private void destroyObstruction(Body obstructionBody) throws JSONException
{
obstructionBody.setActive(false);
JSONObject secondBodyData = null;
if(obstructionBody.getUserData()!=null)
{
secondBodyData=(JSONObject) obstructionBody.getUserData();
//explodeObstruction(((IEntity) secondBodyData.get("sprite")).getX(),((IEntity) secondBodyData.get("sprite")).getY());
if(secondBodyData.get("sprite") instanceof AnimatedSprite)
{
removeObject((AnimatedSprite) secondBodyData.get("sprite"));
}
else
{
removeObject((Sprite) secondBodyData.get("sprite"));
}
}
}
private void removeObject(final AnimatedSprite myAnimSprite)
{
final PhysicsConnector myPhysicsConnector = this.mPhysicsWorld.getPhysicsConnectorManager().findPhysicsConnectorByShape(myAnimSprite);
this.mPhysicsWorld.unregisterPhysicsConnector(myPhysicsConnector);
this.mPhysicsWorld.destroyBody(myPhysicsConnector.getBody());
this.mScene.unregisterTouchArea(myAnimSprite);
this.mScene.detachChild(myAnimSprite);
System.gc();
}
private void removeObject(final Sprite mySprite)
{
final PhysicsConnector myPhysicsConnector = this.mPhysicsWorld.getPhysicsConnectorManager().findPhysicsConnectorByShape(mySprite);
this.mPhysicsWorld.unregisterPhysicsConnector(myPhysicsConnector);
this.mPhysicsWorld.destroyBody(myPhysicsConnector.getBody());
this.mScene.unregisterTouchArea(mySprite);
this.mScene.detachChild(mySprite);
System.gc();
}
I would like to take a look at your objects creating code. I assume every sprite has the same TextureRegion used, so when the region of one sprite is being changed - same regions on other sprites are being changed too due to Android architecture specifics. For every sprite with same TextureRegion you should use textureRegion.clone() as the last parameter of the constructor. Hope this helps.
I'm new to Android development, and I've been playing around with it a bit. I was trying to create a program that has a small database-like collection of never-changing data. In C#, my currently best language, I'd use a List of a custom class and serialize that to an xml file, then read that into my application at runtime. I found the /xml resource folder in Android, but I'm not sure how I would go about doing what I'm envisioning. What would be the best way to go about doing this?
The data will never need to change. Example:
Blob | A | B
----------------
Blob 1 | 23 | 42
Blob 2 | 34 | 21
I know that's laid out like a table, but using a database doesn't really make sense to me because the data will never change, and I would need a way to store it to initially populate the database anyway.
So basically I'm looking for a way to store somewhat-complex static data in my application. Any ideas?
EDIT: I also saw the /raw folder. So I could store things in /res/raw or /res/xml. But I'm not sure what would be the best way to store/parse the data...
I think this is the BEST solution and i am already using this one to store Static-data in my every project.
For that...
You can do one thing, make one xml file namely "temp.xml" ..and store the data in temp.xml as follows:
<?xml version="1.0" encoding="utf-8"?>
<rootelement1>
<subelement> Blob 1
<subsubelement> 23 </subsubelement>
<subsubelement> 42 </subsubelement>
</subelement>
<subelement>Blob 2
<subsubelement> 34 </subsubelement>
<subsubelement> 21 </subsubelement>
</subelement>
</rootelement1>
and then use XML PullParser technique to parse data.
You can have coding samples of PullParsing technique on Example , refer this example for better idea.
Enjoy!!
The best way is to use the Android Resource Heirarchy.
In the res/values/ directory, you can store any number of key-value pairs for several basic data types. In your app, you would refer to them using an autogenerated resource id (name based on your resource's key). See the link above for more documentation and details.
Android also supports raw datafiles. You could store your data in the file directory under res/raw/yourfile.dat
You you create your data in whatever text based format you want and then read it on activity startup using the resource access apis.
I have used Simple for xml parsing in the past. I think it has the least amount of code if you know what to expect in xml, which in your case you do.
http://simple.sourceforge.net/
According to the doc, /xml is the way to go.
Providing Resources
xml/ Arbitrary XML files that can be read at run-time by calling
Resources.getXML().
Various XML configuration files must be saved here, such as a searchable configuration.
Documentation for getXML()
I also made a working example:
the XML structure:
<?xml version="1.0" encoding="utf-8"?>
<quizquestions>
<quizquestion>
<header_image_src>ic_help_black_24dp</header_image_src>
<question>What is the Capital of U.S.A.?</question>
<input_type>Radio</input_type>
<answer correct="false">New York City</answer>
<answer correct="true">Washington D.C.</answer>
<answer correct="false">Chicago</answer>
<answer correct="false">Philadelphia</answer>
</quizquestion>
<quizquestion>
<header_image_src>ic_help_black_24dp</header_image_src>
<question>What is the family name of the famous dutch painter Vincent Willem van .... ?</question>
<input_type>EditText</input_type>
<answer correct="true">Gogh</answer>
</quizquestion>
</quizquestions>
the Java class to hold parsed data:
public class QuizQuestion {
private int headerImageResId;
private String question;
private String inputType;
private ArrayList<String> answers;
private ArrayList<Boolean> answerIsCorrect;
private ArrayList<Integer> correctAnswerIndexes;
/**
* constructor for QuizQuestion object
*/
QuizQuestion() {
headerImageResId = 0;
question = null;
inputType = null;
answers = new ArrayList<>();
answerIsCorrect = new ArrayList<>();
correctAnswerIndexes = new ArrayList<>();
}
public void setHeaderImageResId(int headerImageResId) {
this.headerImageResId = headerImageResId;
}
public int getHeaderImageResId() {
return headerImageResId;
}
void setQuestion(String question) {
this.question = question;
}
public String getQuestion() {
return question;
}
void setInputType(String inputType) {
this.inputType = inputType;
}
public String getInputType() {
return inputType;
}
void addAnswer(String answer, boolean isCorrect)
{
if (isCorrect)
correctAnswerIndexes.add(answers.size());
answers.add(answer);
answerIsCorrect.add(isCorrect);
}
public ArrayList<String> getAnswers() {
return answers;
}
public String getAnswer(int index)
{
// check index to avoid out of bounds exception
if (index < answers.size()) {
return answers.get(index);
}
else
{
return null;
}
}
public int size()
{
return answers.size();
}
}
the parser itself:
/**
* Created by bivanbi on 2017.02.23..
*
* class to parse xml resource containing quiz data into ArrayList of QuizQuestion objects
*
*/
public class QuizXmlParser {
public static String lastErrorMessage = "";
/**
* static method to parse XML data into ArrayList of QuizQuestion objects
* #param activity is the calling activity
* #param xmlResourceId is the resource id of XML resource to be parsed
* #return null if parse error is occurred or ArrayList of objects if successful
* #throws XmlPullParserException
* #throws IOException
*/
public static ArrayList<QuizQuestion> parse(Activity activity, int xmlResourceId)
throws XmlPullParserException, IOException
{
String logTag = QuizXmlParser.class.getSimpleName();
Resources res = activity.getResources();
XmlResourceParser quizDataXmlParser = res.getXml(R.xml.quiz_data);
ArrayList<String> xmlTagStack = new ArrayList<>();
ArrayList<QuizQuestion> quizQuestions = new ArrayList<>();
QuizQuestion currentQuestion = null;
boolean isCurrentAnswerCorrect = false;
quizDataXmlParser.next();
int eventType = quizDataXmlParser.getEventType();
while (eventType != XmlPullParser.END_DOCUMENT)
{
// begin document
if(eventType == XmlPullParser.START_DOCUMENT)
{
Log.d(logTag,"Begin Document");
}
// begin tag
else if(eventType == XmlPullParser.START_TAG)
{
String tagName = quizDataXmlParser.getName();
xmlTagStack.add(tagName);
Log.d(logTag,"Begin Tag "+tagName+", depth: "+xmlTagStack.size());
Log.d(logTag,"Tag "+tagName+" has "+quizDataXmlParser.getAttributeCount()+" attribute(s)");
// this is a beginning of a quiz question tag so create a new QuizQuestion object
if (tagName.equals("quizquestion")){
currentQuestion = new QuizQuestion();
}
else if(tagName.equals("answer"))
{
isCurrentAnswerCorrect = quizDataXmlParser.getAttributeBooleanValue(null,"correct",false);
if (isCurrentAnswerCorrect == true) {
Log.d(logTag, "Tag " + tagName + " has attribute correct = true");
}
else
{
Log.d(logTag, "Tag " + tagName + " has attribute correct = false");
}
}
}
// end tag
else if(eventType == XmlPullParser.END_TAG)
{
String tagName = quizDataXmlParser.getName();
if (xmlTagStack.size() < 1)
{
lastErrorMessage = "Error 101: encountered END_TAG "+quizDataXmlParser.getName()+" while TagStack is empty";
Log.e(logTag, lastErrorMessage);
return null;
}
xmlTagStack.remove(xmlTagStack.size()-1);
Log.d(logTag,"End Tag "+quizDataXmlParser.getName()+", depth: "+xmlTagStack.size());
// reached the end of a quizquestion definition, add it to the array
if (tagName.equals("quizquestion")){
if (currentQuestion != null)
quizQuestions.add(currentQuestion);
currentQuestion = null;
}
}
// text between tag begin and end
else if(eventType == XmlPullParser.TEXT)
{
String currentTag = xmlTagStack.get(xmlTagStack.size()-1);
String text = quizDataXmlParser.getText();
Log.d(logTag,"Text: "+text+", current tag: "+currentTag+", depth: "+xmlTagStack.size());
if (currentQuestion == null) {
Log.e(logTag,"currentQuestion is not initialized! text: "+text+", current tag: "+currentTag+", depth: "+xmlTagStack.size());
continue;
}
if (currentTag.equals("header_image_src"))
{
int drawableResourceId = activity.getResources().getIdentifier(text, "drawable", activity.getPackageName());
currentQuestion.setHeaderImageResId(drawableResourceId);
}
else if (currentTag.equals("question"))
{
currentQuestion.setQuestion(text);
}
else if (currentTag.equals("answer"))
{
currentQuestion.addAnswer(text, isCurrentAnswerCorrect);
}
else if (currentTag.equals("input_type"))
{
currentQuestion.setInputType(text);
}
else
{
Log.e(logTag,"Unexpected tag "+currentTag+" with text: "+text+", depth: "+xmlTagStack.size());
}
}
eventType = quizDataXmlParser.next();
}
Log.d(logTag,"End Document");
return quizQuestions;
}
}
and finally, calling the parser:
// read quiz data from xml resource quiz_data
try {
quizQuestions = QuizXmlParser.parse(this,R.xml.quiz_data);
Log.d("Main","QuizQuestions: "+quizQuestions);
} catch (XmlPullParserException e) {
// TODO Auto-generated catch block
e.printStackTrace();
quizQuestions = null;
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
quizQuestions = null;
}
if (quizQuestions == null)
{
Toast.makeText(this,"1001 Failed to parse Quiz XML, sorry", Toast.LENGTH_LONG).show();
finish();
}