commit f0a462dc08308e17770d468f47cb8f92b1b6320e
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Wed Nov 19 00:21:37 2025 +0100

    Add version to the solves JSON and make sure that if the files is somehow corrupt, it simply gets deleted.

diff --git a/src/main/java/org/distorted/helpers/RememberedSolves.java b/src/main/java/org/distorted/helpers/RememberedSolves.java
new file mode 100644
index 00000000..0b78427d
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/RememberedSolves.java
@@ -0,0 +1,294 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2025 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import android.app.Activity;
+
+import org.distorted.library.type.Static4D;
+import org.distorted.main.MainObjectPopup;
+import org.distorted.objectlib.main.TwistyObject;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+import java.util.Locale;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RememberedSolves
+{
+  private static final int FILE_VERSION = 1;
+  private static final int MAXSOLVES = 4;
+  private static RememberedSolves mThis;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RememberedSolves()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String produceFilename(String objname)
+    {
+    return objname.toLowerCase(Locale.ENGLISH)+"_solves.json";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String readContents(InputStream stream) throws IOException
+    {
+    BufferedReader br = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
+    StringBuilder contents = new StringBuilder();
+    String tmp;
+
+    while( (tmp = br.readLine()) != null) contents.append(tmp);
+    br.close();
+    stream.close();
+
+    return contents.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private JSONObject createSolveData(int elapsed, Static4D rot, TwistyObject object) throws JSONException
+    {
+    JSONObject data = new JSONObject();
+    data.put("time",System.currentTimeMillis());
+    data.put("elapsed", elapsed);
+    data.put("rot0", rot.get0() );
+    data.put("rot1", rot.get1() );
+    data.put("rot2", rot.get2() );
+    data.put("rot3", rot.get3() );
+    JSONObject state = object.generateObjectState();
+    data.put("objState", state);
+
+    return data;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String deleteInfo(InputStream stream, int level, long time)
+    {
+    try
+      {
+      String contents = readContents(stream);
+      JSONObject file = new JSONObject(contents);
+      JSONArray levels = file.getJSONArray("levels");
+      JSONArray lvl = levels.getJSONArray(level);
+      int numSolves = lvl.length();
+
+      for(int s=0; s<numSolves; s++)
+        {
+        JSONObject obj = lvl.getJSONObject(s);
+        long tm = obj.getLong("time");
+        if( tm==time ) { lvl.remove(s); break; }
+        }
+
+      return file.toString();
+      }
+    catch(IOException iex)    { android.util.Log.e("D", "addInfo: failed to read file: "+iex.getMessage() ); }
+    catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file: "+jex.getMessage()); }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String addInfo(InputStream stream, int level, int elapsed, Static4D rot, TwistyObject object)
+    {
+    if( stream!=null )
+      {
+      try
+        {
+        String contents = readContents(stream);
+        JSONObject file = new JSONObject(contents);
+        JSONArray levels = file.getJSONArray("levels");
+        JSONArray lvl = levels.getJSONArray(level);
+        JSONObject data = createSolveData(elapsed,rot,object);
+        if( lvl.length()>=MAXSOLVES || level==0) lvl.remove(0);
+        lvl.put(data);
+        return file.toString();
+        }
+      catch(IOException iex)    { android.util.Log.e("D", "addInfo: failed to read file: "+iex.getMessage() ); }
+      catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file: "+jex.getMessage()); }
+      }
+    else
+      {
+      JSONObject file = new JSONObject();
+      JSONArray levels = new JSONArray();
+
+      try
+        {
+        for(int l=0; l<MainObjectPopup.LEVELS_SHOWN+1; l++)
+          {
+          JSONArray save = new JSONArray();
+
+          if( l==level )
+            {
+            JSONObject data = createSolveData(elapsed,rot,object);
+            save.put(data);
+            }
+          levels.put(save);
+          }
+
+        file.put("version",FILE_VERSION);
+        file.put("levels",levels);
+        }
+      catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file: "+jex.getMessage()); }
+
+      return file.toString();
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static RememberedSolves getInstance()
+    {
+    if( mThis==null ) mThis = new RememberedSolves();
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public long getTime(JSONObject object) throws JSONException
+    {
+    return object.getLong("time");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getElapsed(JSONObject object) throws JSONException
+    {
+    return object.getInt("elapsed");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Static4D getRotQuat(JSONObject object) throws JSONException
+    {
+    float r0 = (float)object.getDouble("rot0");
+    float r1 = (float)object.getDouble("rot1");
+    float r2 = (float)object.getDouble("rot2");
+    float r3 = (float)object.getDouble("rot3");
+
+    return new Static4D(r0,r1,r2,r3);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public JSONObject getObjectState(JSONObject object) throws JSONException
+    {
+    return object.getJSONObject("objState");
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public JSONArray getLevelsArray(Activity act, String objname)
+    {
+    String filename  = produceFilename(objname);
+    RubikFiles files = RubikFiles.getInstance();
+    InputStream file = files.openFile(act,filename);
+
+    if( file==null ) return null;
+
+    try
+      {
+      String contents = readContents(file);
+      JSONObject f = new JSONObject(contents);
+      return f.getJSONArray("levels");
+      }
+    catch(IOException iex)
+      {
+      android.util.Log.e("D", "readFile: failed to read file: "+iex.getMessage() );
+      files.deleteFile(act,filename);
+      }
+    catch(JSONException jex)
+      {
+      android.util.Log.e("D", "readFile: failed to parse file: "+jex.getMessage());
+      files.deleteFile(act,filename);
+      }
+
+    return null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void deleteFile(Activity act, String objname)
+    {
+    String filename  = produceFilename(objname);
+    RubikFiles files = RubikFiles.getInstance();
+    files.deleteFile(act,filename);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void deleteSolve(Activity act, String objname, int level, long time)
+    {
+    String filename  = produceFilename(objname);
+    RubikFiles files = RubikFiles.getInstance();
+    InputStream input= files.openFile(act,filename);
+
+    if( input!=null )
+      {
+      String contents= deleteInfo(input,level,time);
+
+      if( contents!=null )
+        {
+        File file = new File(act.getFilesDir(), filename);
+
+        try( FileOutputStream fos = new FileOutputStream(file) )
+          {
+          fos.write(contents.getBytes(StandardCharsets.UTF_8));
+          }
+        catch(IOException ex)
+          {
+          android.util.Log.e("D", "deleteSolve: failed to save file "+filename+" : "+ex.getMessage());
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void rememberSolve(Activity act, String objname, int level, int elapsed, Static4D rot, TwistyObject object)
+    {
+    String filename  = produceFilename(objname);
+    RubikFiles files = RubikFiles.getInstance();
+    InputStream input= files.openFile(act,filename);
+    String contents  = addInfo(input,level,elapsed,rot,object);
+
+    if( contents!=null )
+      {
+      File file = new File(act.getFilesDir(), filename);
+
+      try( FileOutputStream fos = new FileOutputStream(file) )
+        {
+        fos.write(contents.getBytes(StandardCharsets.UTF_8));
+        }
+      catch(IOException ex)
+        {
+        android.util.Log.e("D", "rememberSolve: failed to save file "+filename+" : "+ex.getMessage());
+        }
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/helpers/RubikRememberedSolves.java b/src/main/java/org/distorted/helpers/RubikRememberedSolves.java
deleted file mode 100644
index ac946844..00000000
--- a/src/main/java/org/distorted/helpers/RubikRememberedSolves.java
+++ /dev/null
@@ -1,273 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2025 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.helpers;
-
-import android.app.Activity;
-
-import org.distorted.library.type.Static4D;
-import org.distorted.main.MainObjectPopup;
-import org.distorted.objectlib.main.TwistyObject;
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-
-import java.io.BufferedReader;
-import java.io.File;
-import java.io.FileOutputStream;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.nio.charset.StandardCharsets;
-import java.util.Locale;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikRememberedSolves
-{
-  private static final int MAXSOLVES = 4;
-  private static RubikRememberedSolves mThis;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private RubikRememberedSolves()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String produceFilename(String objname)
-    {
-    return objname.toLowerCase(Locale.ENGLISH)+"_solves.json";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String readContents(InputStream stream) throws IOException
-    {
-    BufferedReader br = new BufferedReader(new InputStreamReader(stream, StandardCharsets.UTF_8));
-    StringBuilder contents = new StringBuilder();
-    String tmp;
-
-    while( (tmp = br.readLine()) != null) contents.append(tmp);
-    br.close();
-    stream.close();
-
-    return contents.toString();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private JSONObject createData(int elapsed, Static4D rot, TwistyObject object) throws JSONException
-    {
-    JSONObject data = new JSONObject();
-    data.put("time",System.currentTimeMillis());
-    data.put("elapsed", elapsed);
-    data.put("rot0", rot.get0() );
-    data.put("rot1", rot.get1() );
-    data.put("rot2", rot.get2() );
-    data.put("rot3", rot.get3() );
-    JSONObject state = object.generateObjectState();
-    data.put("objState", state);
-
-    return data;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String deleteInfo(InputStream stream, int level, long time)
-    {
-    try
-      {
-      String contents = readContents(stream);
-      JSONArray levels = new JSONArray(contents);
-      JSONArray lvl = levels.getJSONArray(level);
-      int numSolves = lvl.length();
-
-      for(int s=0; s<numSolves; s++)
-        {
-        JSONObject obj = lvl.getJSONObject(s);
-        long tm = obj.getLong("time");
-        if( tm==time ) { lvl.remove(s); break; }
-        }
-
-      return levels.toString();
-      }
-    catch(IOException iex)    { android.util.Log.e("D", "addInfo: failed to read file: "+iex.getMessage() ); }
-    catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file: "+jex.getMessage()); }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String addInfo(InputStream stream, int level, int elapsed, Static4D rot, TwistyObject object)
-    {
-    if( stream!=null )
-      {
-      try
-        {
-        String contents = readContents(stream);
-        JSONArray levels = new JSONArray(contents);
-        JSONArray lvl = levels.getJSONArray(level);
-        JSONObject data = createData(elapsed,rot,object);
-        if( lvl.length()>=MAXSOLVES || level==0) lvl.remove(0);
-        lvl.put(data);
-        return levels.toString();
-        }
-      catch(IOException iex)    { android.util.Log.e("D", "addInfo: failed to read file: "+iex.getMessage() ); }
-      catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file: "+jex.getMessage()); }
-      }
-    else
-      {
-      JSONArray levels = new JSONArray();
-
-      try
-        {
-        for(int l=0; l< MainObjectPopup.LEVELS_SHOWN+1; l++)
-          {
-          JSONArray save = new JSONArray();
-
-          if( l==level )
-            {
-            JSONObject data = createData(elapsed,rot,object);
-            save.put(data);
-            }
-          levels.put(save);
-          }
-        }
-      catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file: "+jex.getMessage()); }
-
-      return levels.toString();
-      }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static RubikRememberedSolves getInstance()
-    {
-    if( mThis==null ) mThis = new RubikRememberedSolves();
-    return mThis;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public long getTime(JSONObject object) throws JSONException
-    {
-    return object.getLong("time");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getElapsed(JSONObject object) throws JSONException
-    {
-    return object.getInt("elapsed");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Static4D getRotQuat(JSONObject object) throws JSONException
-    {
-    float r0 = (float)object.getDouble("rot0");
-    float r1 = (float)object.getDouble("rot1");
-    float r2 = (float)object.getDouble("rot2");
-    float r3 = (float)object.getDouble("rot3");
-
-    return new Static4D(r0,r1,r2,r3);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public JSONObject getObjectState(JSONObject object) throws JSONException
-    {
-    return object.getJSONObject("objState");
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public JSONArray readFile(Activity act, String objname)
-    {
-    RubikFiles files = RubikFiles.getInstance();
-    InputStream file = files.openFile(act,produceFilename(objname));
-
-    if( file==null ) return null;
-
-    try
-      {
-      String contents = readContents(file);
-      return new JSONArray(contents);
-      }
-    catch(IOException iex)    { android.util.Log.e("D", "readFile: failed to read file: "+iex.getMessage() ); }
-    catch(JSONException jex)  { android.util.Log.e("D", "readFile: failed to parse file: "+jex.getMessage()); }
-
-    return null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void deleteFile(Activity act, String objname)
-    {
-    String filename  = produceFilename(objname);
-    RubikFiles files = RubikFiles.getInstance();
-    files.deleteFile(act,filename);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void deleteSolve(Activity act, String objname, int level, long time)
-    {
-    String filename  = produceFilename(objname);
-    RubikFiles files = RubikFiles.getInstance();
-    InputStream input= files.openFile(act,filename);
-
-    if( input!=null )
-      {
-      String contents= deleteInfo(input,level,time);
-      File file      = new File(act.getFilesDir(), filename);
-
-      try( FileOutputStream fos = new FileOutputStream(file) )
-        {
-        fos.write(contents.getBytes(StandardCharsets.UTF_8));
-        }
-      catch(IOException ex)
-        {
-        android.util.Log.e("D", "deleteSolve: failed to save file "+filename+" : "+ex.getMessage());
-        }
-      }
-    else
-      {
-      android.util.Log.e("D", "deleteSolve: error: file "+filename+" not found");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void rememberSolve(Activity act, String objname, int level, int elapsed, Static4D rot, TwistyObject object)
-    {
-    String filename  = produceFilename(objname);
-    RubikFiles files = RubikFiles.getInstance();
-    InputStream input= files.openFile(act,filename);
-    String contents  = addInfo(input,level,elapsed,rot,object);
-    File file        = new File(act.getFilesDir(), filename);
-
-    try( FileOutputStream fos = new FileOutputStream(file) )
-      {
-      fos.write(contents.getBytes(StandardCharsets.UTF_8));
-      }
-    catch(IOException ex)
-      {
-      android.util.Log.e("D", "rememberSolve: failed to save file "+filename+" : "+ex.getMessage());
-      }
-    }
-}
diff --git a/src/main/java/org/distorted/main/MainObjectPopup.java b/src/main/java/org/distorted/main/MainObjectPopup.java
index 043e7abf..00d8c64b 100644
--- a/src/main/java/org/distorted/main/MainObjectPopup.java
+++ b/src/main/java/org/distorted/main/MainObjectPopup.java
@@ -26,7 +26,7 @@ import android.widget.TextView;
 
 import androidx.annotation.NonNull;
 
-import org.distorted.helpers.RubikRememberedSolves;
+import org.distorted.helpers.RememberedSolves;
 import org.distorted.helpers.RubikScores;
 import org.distorted.library.type.Static4D;
 import org.distorted.objects.RubikObject;
@@ -82,8 +82,8 @@ public class MainObjectPopup
       {
       public void run()
         {
-        RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
-        mRememberedSolves = solves.readFile(act,objname);
+        RememberedSolves solves = RememberedSolves.getInstance();
+        mRememberedSolves = solves.getLevelsArray(act,objname);
         }
       };
 
@@ -296,7 +296,7 @@ public class MainObjectPopup
             {
             if( numSolves>0 )
               {
-              RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
+              RememberedSolves solves = RememberedSolves.getInstance();
 
               try
                 {
diff --git a/src/main/java/org/distorted/main/MainSolvesPopup.java b/src/main/java/org/distorted/main/MainSolvesPopup.java
index f279ba42..54456988 100644
--- a/src/main/java/org/distorted/main/MainSolvesPopup.java
+++ b/src/main/java/org/distorted/main/MainSolvesPopup.java
@@ -21,7 +21,7 @@ import android.widget.LinearLayout;
 import android.widget.PopupWindow;
 import android.widget.TextView;
 
-import org.distorted.helpers.RubikRememberedSolves;
+import org.distorted.helpers.RememberedSolves;
 import org.distorted.library.type.Static4D;
 import org.distorted.objects.RubikObject;
 import org.json.JSONArray;
@@ -80,7 +80,7 @@ public class MainSolvesPopup
     LinearLayout.LayoutParams pT = new LinearLayout.LayoutParams( LinearLayout.LayoutParams.MATCH_PARENT, textH );
 
     int numSolves = array.length();
-    RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
+    RememberedSolves solves = RememberedSolves.getInstance();
 
     try
       {
@@ -154,7 +154,7 @@ public class MainSolvesPopup
           {
           mLayout.removeView(view);
           int level = mLevel==LEVELS_SHOWN ? mLevel : mLevel+1;
-          RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
+          RememberedSolves solves = RememberedSolves.getInstance();
           solves.deleteSolve(act, mObject.getLowerName(), level, time);
           }
         });
diff --git a/src/main/java/org/distorted/play/PlayActivity.java b/src/main/java/org/distorted/play/PlayActivity.java
index 24c88e69..dd68fe5f 100644
--- a/src/main/java/org/distorted/play/PlayActivity.java
+++ b/src/main/java/org/distorted/play/PlayActivity.java
@@ -27,7 +27,7 @@ import com.google.firebase.analytics.FirebaseAnalytics;
 
 import org.distorted.dialogs.DialogScores;
 import org.distorted.helpers.BaseActivity;
-import org.distorted.helpers.RubikRememberedSolves;
+import org.distorted.helpers.RememberedSolves;
 import org.distorted.library.main.DistortedLibrary;
 import org.distorted.library.type.Static4D;
 import org.distorted.objectlib.main.InitAssets;
@@ -369,7 +369,7 @@ public class PlayActivity extends BaseActivity implements DialogScores.ScoresInv
         {
         public void run()
           {
-          RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
+          RememberedSolves solves = RememberedSolves.getInstance();
           solves.rememberSolve(act,name,level,time,rotQuat,object);
           }
         };
