I am making a simple game that uses GridView and a custom Adapter. It is basically a game that player can move through the GridView (simply changing the images of cells). The game has 10 levels. Problem is, when I get out of the activity (e.g. going back to the MainActivity) the game is reset. Also naturally when phone is turend off and on the game is reset.
I want to preserve the game state so when the player enters the GameActivity, he/she can continue with the game.
I only require 3 things to be saved, the Adapter, number of Level and the available moves. Simply if I knew how to work this out I could achieve what I need:
public class GameState implements Serializable {
private GameAssetAdapter mAdapter;
private int mLevel;
private int mAvailableMoves;
public GameState(GameAssetAdapter adapter, int level, int availableMoves) {
mAdapter = adapter;
mLevel = level;
mAvailableMoves = availableMoves;
}
public GameAssetAdapter getAdapter() {
return mAdapter;
}
public int getLevel() {
return mLevel;
}
public int getAvailableMoves() {
return mAvailableMoves;
}
}
So the question is, how can I save this object to internal storage and retrive it back when necessary?
I already have tried the onSaveInstanceState but it does not work as expected. phone off/on will reset this. Even if user wipes the app in the app list of android it will be reset. What should I do?
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putSerializable(AppConstants.GAME_SAVE_ASSET_ADAPTER, mGameAssetAdapter);
}
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_game);
if(savedInstanceState != null)
{ //Restore mGameAssetAdapter if was saved perviously
if(mGameAssetAdapter == null){
restoreGameAssetAdapter(savedInstanceState);
}
}
//TODO get level and states!!
mGameGridView = (GameGridView)findViewById(R.id.game_grid_view);
mGameGridView.setNumColumns(GameConstants.COLUMNS);
mGameGridView.setColumnWidth(GameHelper.getOptimumAssetImageWidth(this,
GameConstants.COLUMNS));
if(mGameAssetAdapter == null) {
mGameAssetAdapter = new GameAssetAdapter(this, mLevel);
}
mGameGridView.setAdapter(mGameAssetAdapter);
this.setTitle("Snakes and Ladders - Level " + mLevel);
setupEvents();
}
private void restoreGameAssetAdapter(Bundle savedInstanceState) {
if(savedInstanceState.getSerializable("GAME_ASSET_ADAPTER") != null){
mGameAssetAdapter =
(GameAssetAdapter) savedInstanceState.
getSerializable("GAME_ASSET_ADAPTER");
Log.v(TAG, "Restored saved GameAssetAdapter! Hoooray!");
}
}
You can just write the state to storage. Here is some code from my personal stash:
private static byte[] readBytes(String dir, Context context) throws IOException
{
FileInputStream fileIn = null;
DataInputStream in = null;
byte[] buffer = null;
fileIn = context.openFileInput(dir);
in = new DataInputStream(fileIn);
int length = in.readInt();
buffer = new byte[length];
for(int x = 0;x < buffer.length;x++)
buffer[x] = in.readByte();
try
{
fileIn.close();
} catch (Exception e) {}
try
{
in.close();
} catch (Exception e) {}
fileIn = null;
in = null;
return buffer;
}
private static void writeBytes(String dir, byte bytes[], Context context) throws IOException
{
FileOutputStream fileOut = null;
DataOutputStream out = null;
fileOut = context.openFileOutput(dir, Context.MODE_PRIVATE);
out = new DataOutputStream(fileOut);
int length = bytes.length;
out.writeInt(length);
out.write(bytes);
out.flush();
try
{
fileOut.close();
} catch (Exception e) {}
try
{
out.close();
} catch (Exception e) {}
}
You can save your state by using the writeBytes() method, then when the app is relaunched, all you have to do is use readBytes() to restore the game state.
If I may make one small structural suggestion, I like to make a 'package' class that holds my state variables when I write it to disk like this:
public class SavedState implements Serializible
{
public GameState state;
int id;
...
}
Then when you write this to disk, you will have all of your state variables in one clean class. I would also recommend not saving your DisplayAdapter for your list view (you might have a lot of problems) or whatever it is. Save the underlying data structure to this package class and then create a new DisplayAdapter when you resume the app state.
If you don't know how to turn an object into a byte array, here is the SO question for it:
Java: object to byte[] and byte[] to object converter (for Tokyo Cabinet)
Related
I am creating a simple quiz application where the user answers five questions and a result is toasted onto the screen. In my MainActivity, I call the constructor of a questionsMethods class with parameters context and activity.
My question is: How do I get the context and activity of MainActivity in a JUnit test class? I need this because the methods in questionsMethods make use of context and activity.
Part of the code:
MainActivity:
public class MainActivity extends AppCompatActivity {
questionsMethods myQuestionMethods = new questionsMethods(this, this);
private String[] questionArray = new String[30]; // Holds text file
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
AssetManager assetManager = getAssets();
myQuestionMethods.readWriteFile("listofquestions.txt", questionArray); // Reads from text file into array
questionsMethods:
public class questionsMethods {
private Context mContext;
private Activity mActivity;
private int localCounterForCurrQuestionCount = 1;
private String[] localQuestionsArray = new String[30];
public questionsMethods(Context context, Activity activity) {
mContext = context;
mActivity = activity;
}
public void readWriteFile(String fileName, String[] questionArray) {
int count = 0; // Holds count of array index in which a line is stored
try {
String nextLine;
InputStream is = mContext.getAssets().open(fileName); // Retrieves and opens fileName
BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(is));
while ((nextLine = bufferedReader.readLine()) != null) { // Check if line incrementer reaches the end
questionArray[count] = nextLine;
count++;
}
} catch (java.io.IOException ex) {
Log.i("Error", "Cannot read file");
System.exit(1);
}
localQuestionsArray = questionArray;
}
Use Robolectric, however your code is very hard to test.
The QuestionMethods class is only responsible for business logic and should not depend on Android.
So if you want to make your code cleaner and you want to test QuestionMethods properly,
Robolectric should not be used for classes that are responsible for business logic.
I'm just trying to understand the Activity lifetime with this simple example by trying to find how I get back the SomeIntegers g_values object and the ArrayList AnInteger objects within it.
As it is, it is not of much meaning but will serve as a paradigm of my real situation where initial setup requires the app to schlepp through countless reams of pre-processing eg access and list fonts, analyse all my available games, in the APK, on file and online in my website, players records etc. The final app is a system of games and activities to help SpLD (dyslexia) students of all ages exercise their reading, spelling, organisational skills and short term memory. It is of serious intent. Although free running, it is best used with SpLD supervisors/tutors who can set the work schedule of their charge and even add their own games.
Anyway enough of the irrelevant background.
Can I save my somewhat complex objects using access to the savedInstanceState (somewhat hampered by their being no putxxxxx method of the correct form) or should abandon this approach and recover the data from persistent files or databases? This can be discussed hopefully within the limits of this simple example, the real thing is simply more of the same but with different details.
Note added after. There is also the issue of taking the user/player back to where he/she was when the app experienced the need to save its InstanceState. As the major influence seems to be the orientation of the tablet, I could maybe side step that by locking the orientation at start up. This would simplify many display issues also but is it an "unacceptable" style?
import android.os.Parcelable;
import android.support.v7.app.AppCompatActivity;
import android.os.Bundle;
public class TestBundle extends AppCompatActivity {
SomeIntegers g_values;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
System.out.println("onCreate (" + (savedInstanceState == null ? "null)" : "set)"));
if (savedInstanceState == null)
{ g_values = new SomeIntegers();
String result = g_values.report();
System.out.println("Startup Result: " + result);
setContentView(R.layout.activity_test_bundle); // Where do I put this line?
}
else
{ //Do I get g_values back here?
//More relevantly, can I, and how can I, put g_values in the
//savedInstanceState when onSaveInstanceState is called?
String result = g_values.report();
System.out.println("Result: " + result);
}
}
#Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
System.out.println("onSaveInstanceState (" + (outState == null ? "null)" : "set)"));
//How do I add g_values to the Bundle?
}
// Following is just stuff to watch the progress of the
// Activity in the ADB Log. Not of much relevance. Or is it?
#Override
protected void onStop() {
super.onStop();
System.out.println("onStop()");
}
#Override
protected void onStart() {
super.onStart();
System.out.println("onStart()");
}
#Override
protected void onRestart() {
super.onRestart();
System.out.println("onRestart()");
}
#Override
protected void onResume() {
super.onResume();
System.out.println("onResume()");
}
#Override
protected void onPause() {
super.onPause();
System.out.println("onPause()");
}
#Override
protected void onDestroy() {
super.onDestroy();
System.out.println("onDestroy()");
}
}
public class SomeIntegers {
private ArrayList<AnInteger> c_values;
SomeIntegers() {
c_values = new ArrayList<AnInteger>();
c_values.add (new AnInteger(1));
c_values.add (new AnInteger(2));
c_values.add (new AnInteger(3));
c_values.add (new AnInteger(4));
c_values.add (new AnInteger(29));
c_values.add (new AnInteger(30));
}
String report() {
String g = "";
for (AnInteger ai : c_values) {
if (!g.isEmpty()) g = g + ", ";
g = g + ai.getC_value();
}
return (g.isEmpty() ? "Empty" : g);
}
}
public class AnInteger {
private int c_value;
AnInteger(int value) { c_value = value); }
public int getC_value () { return c_value; }
}
Thank you. Josie Hill
First make your data models implement Parcelable :
AnInteger:
public class AnInteger implements Parcelable {
private int c_value;
public AnInteger(int value) {
this.c_value = value;
}
public int getC_value() {
return c_value;
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeInt(this.c_value);
}
protected AnInteger(Parcel in) {
this.c_value = in.readInt();
}
public static final Parcelable.Creator<AnInteger> CREATOR = new Parcelable.Creator<AnInteger>() {
#Override
public AnInteger createFromParcel(Parcel source) {
return new AnInteger(source);
}
#Override
public AnInteger[] newArray(int size) {
return new AnInteger[size];
}
};
}
SomeIntegers:
public class SomeIntegers implements Parcelable {
private ArrayList<AnInteger> c_values;
public SomeIntegers() {
c_values = new ArrayList<>();
c_values.add(new AnInteger(1));
c_values.add(new AnInteger(2));
c_values.add(new AnInteger(3));
c_values.add(new AnInteger(4));
c_values.add(new AnInteger(29));
c_values.add(new AnInteger(30));
}
public String report() {
String g = "";
for (AnInteger ai : c_values) {
if (!g.isEmpty()) {
g = g + ", ";
}
g = g + ai.getC_value();
}
return (g.isEmpty() ? "Empty" : g);
}
#Override
public int describeContents() {
return 0;
}
#Override
public void writeToParcel(Parcel dest, int flags) {
dest.writeTypedList(this.c_values);
}
protected SomeIntegers(Parcel in) {
this.c_values = in.createTypedArrayList(AnInteger.CREATOR);
}
public static final Parcelable.Creator<SomeIntegers> CREATOR = new Parcelable.Creator<SomeIntegers>() {
#Override
public SomeIntegers createFromParcel(Parcel source) {
return new SomeIntegers(source);
}
#Override
public SomeIntegers[] newArray(int size) {
return new SomeIntegers[size];
}
};
}
Then in your activity saving and restoring gets pretty easy, here is an example using your current data model:
//set up class fields/members
private final static String STATE_G_VALS = "STATE_G_VALS";
SomeIntegers g_values = null;
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_test_bundle);
System.out.println("onCreate (" + (savedInstanceState == null ? "null)" : "set)"));
if (savedInstanceState != null) {
// get g_values back here
g_values = savedInstanceState.getParcelable(STATE_G_VALS);
}
if (g_values == null) {
// ok its null, lets make one
g_values = new SomeIntegers();
}
// log some stuff
String result = g_values.report();
System.out.println("Result: " + result);
}
#Override
protected void onSaveInstanceState(Bundle outState) {
//set g_values to the Bundle/saved state (even if it is null)
outState.putParcelable(STATE_G_VALS, g_values);
super.onSaveInstanceState(outState);
}
The concept of restoring the activity states is based on device orientation. So for example if you pull some changes from persisted file, loaded it, when the screen changes it angle of rotation that data will be recreated. So the activity uses a bundle to wrap that data, and permits the user to save the current working state of such file, which then can be restored. Here is a great link. Your requirements sounds consistent as it regards data changes, as per my first question regarding the anticipated file sizes, your requirements sounds relatively small.
To work compound data types and abstract data types , do consider using GSON.which is a Java serialization/deserialization library to convert Java Objects into JSON and back
Therefore I can recommend you using the power of shared preferences in android.If you have a relatively small collection of key-values that you'd like to save, you should use the SharedPreferences APIs. A SharedPreferences object points to a file containing key-value pairs and provides simple methods to read and write them. In simple terms,Shared Preferences allow you to save and retrieve data in the form of key,value pair.
Android provides many ways of storing data of an application. If your requirements needs storage consistency, I would go with the database approach, I would recommend using realm.Realm is a mobile database and a replacement for SQLite. Although is an OO database it has some differences with other databases. Realm is not using SQLite as it’s engine. Instead it has own C++ core and aims to provide a mobile-first alternative to SQLite.
Hope this was helpful:)
I am making an android application that can communicate with arduino serially over USB and used UsbSerial library from Github.
I'm receiving data from arduino in UsbReadCallback which I have implemented as:
private UsbSerialInterface.UsbReadCallback mCallback = new UsbSerialInterface.UsbReadCallback() {
#Override
public void onReceivedData(byte[] arg0) {
try {
String data = new String(arg0, Charset.forName("utf-8"));
if (mHandler != null)
{
mHandler.obtainMessage(0, data).sendToTarget();
}
} catch (Exception e) {
e.printStackTrace();
}
}
};
and my Handler as:
private static class MyHandler extends Handler {
private final WeakReference<SensorDisplayActivity> mActivity;
public MyHandler(SensorDisplayActivity activity) {
mActivity = new WeakReference<>(activity);
}
#Override
public void handleMessage(Message msg) {
String data = ( String) msg.obj;
mActivity.get().mainValue_tx.setText(data);
}
}
But problem arriving when I'm displaying it in the TextView using setText() its not showing the correct value i.e. sometimes showing 00 in place of 100 and 2 in place of 42 etc. but when I'm using append() method of TextView the correct values are appending correctly. What might be the solution?
So I'm trying to make my first ever Android app. It's just a simple tally counter for now but I've come across a weird bug.
You see, I've got a simple save and load function, an increment, decrement and reset button. and a TextView that displays the value all together (see below).
Now when I increment the value up to say 10 and close the app, it saves as it should and when I open the app again, it does come back as 10. However, when I then increment up to say 30 or 100, close and restart the value does not stick and comes out as either -1 or a value that is completely different.
What could be happening?
MainActivity.java
public class MainActivity extends AppCompatActivity {
private static final String TAG = MainActivity.class.getSimpleName();
int value;
TextView textView_value;
final String filename = "tallyCountPlus";
#Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
loadValue(filename);
textView_value = (TextView) findviewById(R.id.textView_value);
textView_value.setText(Integer.toString(value));
}
/** Called when the user presses the increment button */
public void incValue(View view) {
value++;
textView_value.setText(Integer.toString(value));
}
/** Called when the user presses the decrement button */
public void decValue(View view) {
if (value > 0) {
value--;
}
textView_value.setText(Integer.toString(value));
}
/** Called when the user presses the reset button */
public void resetValue(View view) {
value = 0;
textView_value.setText(Integer.setString(value));
}
#Override
public void onPause() {
super.onPause();
saveValue(filename, Context.MODE_PRIVATE);
}
#Override
public void onResume() {
super.onResume();
loadValue(filename);
textView_value.setText(Integer.toString(value));
}
public void onDestroy() {
super.onDestroy();
saveValue(filename, Context.MODE_PRIVATE);
}
private void loadValue(String name) {
try {
FileInputStream fis = openFileInput(name);
value = fis.read();
fis.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
private void saveValue(String name, int context) {
try {
FileOutputStream fos = openFileOutput(name, context);
fos.write(value);
fos.close();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
Anyone know what my problem could be?
Thanks.
Jamie.
EDIT:
So I think I've fixed the bug by doing:
private void loadValue(String filename) {
/* this is wrapped in a try-catch statement */
FileInputStream fis = openFileInput(name);
InputStreamReader isr = new InputStreamReader(fis);
BufferedReader br = new BufferedReader(isr);
StringBuilder = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
value = Integer.parseInt(sb.toString());
fis.close() /* Not sure if this is actually neccessary? */
if (value == -1) {
value = 0;
}
}
private void saveValue(String name) {
/* This is also wrapped in a try-catch statement */
FileOutputStream fos = openFileOutput(name, Context.MODE_PRIVATE);
String data = Integer.toString(value);
fos.write(data.getBytes());
fos.close();
}
And after putting in a couple of values, it seems to work fine. However, I'm unsure as to whether it is appending or deleting the previous value and replacing it... I'd rather the latter to prevent the save file from getting too big.
onDestroy() is not dependable. Just stick with onPause(). Also, in general, for the methods that handle events when the activity is "going away", you should call the super method after your own code, to ensure your custom code can still execute.
Other things to check might be:
Is the method that saves the output replacing the previous file, or just appending to it?
Are you properly loading and saving each time?
If the file is properly loaded, is the value making it into the TextView? You might need to construct an anonymous Runnable that ensures that the TextView is updated by the main (UI) thread.
Sprinkle log statements liberally throughout the code to keep track of what the tally is, what you get out of the file, etc.
When diagnosing file issues, it helps to test on an emulator, which gives you root access to the device storage.
You need to modify loadValue() method. You are assigning fis.read() to value which returns next byte or -1 if EOF reached.
private void loadValue(String name) {
try {
FileInputStream in = openFileInput(name);
InputStreamReader inputStreamReader = new InputStreamReader(in);
BufferedReader bufferedReader = new BufferedReader(inputStreamReader);
StringBuilder sb = new StringBuilder();
String line;
while ((line = bufferedReader.readLine()) != null) {
sb.append(line);
}
value = Integer.parseInt(sb.toString());
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
I'm creating a text file with timestamps derived from the result of System.currentTimeMillis(). My algorithm is such:
Create file and record timestamp of creation
Save timestamp each time a button is pressed
Subtract file-creation timestamp from button-press timestamp
Write result to the file
For some odd reason, the timestamp of the file creation is usually greater (younger, more recent) than the timestamp of the button presses, which always happen AFTER the file was created. This results in a negative value being returned from step 3.
What is causing this?
FileCreationMenu.java
public class FileCreationMenu extends Fragment {
public Button toggleRecordingButton;
#Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.menu_fragment_calibrator, container, false);
toggleRecordingButton = (Button) v.findViewById(R.id.recordAudioToggle);
toggleRecordingButton.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
recordAudio = !recordAudio; //toggle audio recording on/off
if (recordAudio==true){
AudioRecorder.createFile(System.currentTimeMillis()); //generate file for new track if record is togged ON
}
}
});
AudioRecorder.java
public static void createFile(long time) { //create file and record creation timeStamp
recordingStartTime = time;
myFile = new File(Environment.getExternalStorageDirectory(), "file.txt"); //save to external storage
try {
myFile.createNewFile();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void recordNote(long timeStamp){ //record timeStamps of notes
String playedNote = (timeStamp+ "\n" );
try {
fos = new FileOutputStream(myFile, true);
fos.write(playedNote.getBytes());
fos.flush();
} catch (IOException e) {
e.printStackTrace();
}
}
public static void playTrack(String fileName){ //playback Notes
try {
FileInputStream fis = new FileInputStream(myFile.getPath());
BufferedReader reader = new BufferedReader(new InputStreamReader(fis));
String line;
try {
int i =0;
while ((line = reader.readLine()) != null) { //read each line of input file
historicalTime[i] = Long.parseLong(line); //store time current line's note was played
if (i==0){
timeStream[i] = (Long.parseLong(line) - recordingStartTime); //if first note, calculate wait based on file creation time
}
else{
timeStream[i] = (Long.parseLong(line) - historicalTime[i-1]); //otherwise, calculate wait as amount of time note was played after most recent preceding note
}
i++;
}
} catch (IOException e){
e.printStackTrace();
}
for (int i=0; i<noteStream.length; i++){
try {
AudioRecorder.class.wait(timeStream[i]); //wait
//Play Note
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
MainActivity.java
...
public static void noteDetected(){
if (FileCreationMenu.recordAudio == true){
AudioRecorder.recordNote(System.currentTimeMillis());
}
Again, the problem is that recordingStartTime in AudioRecorder.java is often greater than the input parameter "timeStamp" to recordNote from the MainActivity. For example, in a recent debugging session recordingStartTime =1,450,573,093,044 while timeStamp=1,450,565,187,318.
What could be causing this seemingly impossible behavior?
The problem is this line:
myFile.createNewFile();
The JavaDoc for this method says
Atomically creates a new, empty file named by this abstract pathname if and only if a file with this name does not yet exist
So every time you try to start a new recording you are actually reusing the same file (with timestamps from long ago) but record a new recordStartTime.
If you want to remove the old file first you will have to write
myFile.delete();
myFile.createNewFile();