commit e2448de9d601b4fa558fff21825f001d753c769b
Author: leszek <leszek@koltunski.pl>
Date:   Wed Nov 12 16:05:43 2025 +0100

    progress with saving solves

diff --git a/src/main/java/org/distorted/helpers/RubikRememberedSolves.java b/src/main/java/org/distorted/helpers/RubikRememberedSolves.java
new file mode 100644
index 00000000..935151c3
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/RubikRememberedSolves.java
@@ -0,0 +1,131 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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 org.distorted.library.type.Static4D;
+import org.distorted.main.MainObjectPopup;
+import org.json.JSONArray;
+import org.json.JSONException;
+import org.json.JSONObject;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.nio.charset.StandardCharsets;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikRememberedSolves
+{
+  private static final int MAXSOLVES = 8;
+  private static RubikRememberedSolves mThis;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RubikRememberedSolves()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static RubikRememberedSolves getInstance()
+    {
+    if( mThis==null ) mThis = new RubikRememberedSolves();
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private JSONObject createData(long time, Static4D rot, int[] quats) throws JSONException
+    {
+    JSONObject data = new JSONObject();
+    data.put("time",time);
+    data.put("rot0", rot.get0() );
+    data.put("rot1", rot.get1() );
+    data.put("rot2", rot.get2() );
+    data.put("rot3", rot.get3() );
+    JSONArray qs = new JSONArray();
+    for(int q:quats) qs.put(q);
+    data.put("quats", qs);
+
+    return data;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String addInfo(InputStream stream, int level, long time, Static4D rot, int[] quats)
+    {
+    /*
+    StringBuilder quatStr = new StringBuilder();
+
+    for(int c : quats)
+      {
+      quatStr.append(c);
+      quatStr.append(" ");
+      }
+
+    android.util.Log.e("D",   "level: "+level+"\n"+
+                                    "time: "+time+"\n"+
+                                    "rotQuat: " +rot.get0()+" "+rot.get1()+" "+rot.get2()+" "+rot.get3()+"\n"+
+                                    "quats: "+quatStr );
+*/
+    if( stream!=null )
+      {
+      try
+        {
+        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();
+
+        JSONArray levels = new JSONArray(contents.toString());
+        JSONArray lvl = levels.getJSONArray(level);
+        JSONObject data = createData(time,rot,quats);
+        if( lvl.length()>MAXSOLVES ) lvl.remove(0);
+        lvl.put(data);
+
+        return levels.toString();
+        }
+      catch(IOException iex)    { android.util.Log.e("D", "addInfo: failed to read file"); }
+      catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file"); }
+      }
+    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(time,rot,quats);
+            save.put(data);
+            }
+          levels.put(save);
+          }
+        }
+      catch(JSONException jex)  { android.util.Log.e("D", "addInfo: failed to parse file"); }
+
+      return levels.toString();
+      }
+
+    return null;
+    }
+}
diff --git a/src/main/java/org/distorted/main/MainActivity.java b/src/main/java/org/distorted/main/MainActivity.java
index a2f6444d..99387f72 100644
--- a/src/main/java/org/distorted/main/MainActivity.java
+++ b/src/main/java/org/distorted/main/MainActivity.java
@@ -47,6 +47,7 @@ import org.distorted.patterns.PatternActivity;
 import org.distorted.play.PlayActivity;
 import org.distorted.solvers.SolverActivity;
 import org.distorted.tutorials.TutorialActivity;
+import org.json.JSONArray;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -347,6 +348,7 @@ public class MainActivity extends BaseActivity implements RubikNetwork.Updatee,
       intent.putExtra("scrambles", scrambles);
       intent.putExtra("local", local );
       intent.putExtra("ordinal", ordinal );
+
       startActivity(intent);
       }
 
@@ -442,6 +444,6 @@ public class MainActivity extends BaseActivity implements RubikNetwork.Updatee,
 
     public void errorUpdate()
       {
-      android.util.Log.e("D", "NewRubikActivity: Error receiving downloaded objects update");
+      android.util.Log.e("D", "RubikActivity: Error receiving downloaded objects update");
       }
 }
diff --git a/src/main/java/org/distorted/main/MainObjectPopup.java b/src/main/java/org/distorted/main/MainObjectPopup.java
index b94937d3..582b20f4 100644
--- a/src/main/java/org/distorted/main/MainObjectPopup.java
+++ b/src/main/java/org/distorted/main/MainObjectPopup.java
@@ -24,9 +24,12 @@ import android.widget.LinearLayout;
 import android.widget.PopupWindow;
 import android.widget.TextView;
 
+import org.distorted.helpers.RubikRememberedSolves;
 import org.distorted.helpers.RubikScores;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
+import org.json.JSONArray;
+import org.json.JSONException;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -42,6 +45,7 @@ public class MainObjectPopup
   private final PopupWindow mPopup;
   private final int mMenuTextSize;
   private final int mObjectOrdinal;
+  private JSONArray mRememberedSolves = null;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -68,6 +72,18 @@ public class MainObjectPopup
     int levelHeight = (int)(width*BUTTON_HEIGHT);
 
     RubikObject object = RubikObjectList.getObject(ordinal);
+    String objname = object!=null ? object.getLowerName() : "null";
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
+        mRememberedSolves = solves.readFile(objname);
+        }
+      };
+
+    thread.start();
 
     ///////// SOLVER //////////////////////////////////////////////////
     Button b1 = layout.findViewById(R.id.objectSolver);
@@ -248,6 +264,24 @@ public class MainObjectPopup
           mPopup.dismiss();
           if( ll<0 ) scores.setRecord(mObjectOrdinal,ll,0); // remember we've entered the 'Free'
                                                             // so that the button turns green
+
+          JSONArray json = null;
+          int numSolves = 0;
+
+          if( mRememberedSolves!=null )
+            {
+            try
+              {
+              json = mRememberedSolves.getJSONArray(ll);
+              numSolves = json.length();
+              }
+            catch(JSONException jex)
+              {
+              android.util.Log.e("D", "execption trying to parse file: "+jex.getMessage() );
+              }
+            }
+
+          if( numSolves>0 ) act
           act.switchToPlay(object,mObjectOrdinal,scrambles,ll);
           }
         });
diff --git a/src/main/java/org/distorted/play/PlayActivity.java b/src/main/java/org/distorted/play/PlayActivity.java
index 81c0917e..b3fb135f 100644
--- a/src/main/java/org/distorted/play/PlayActivity.java
+++ b/src/main/java/org/distorted/play/PlayActivity.java
@@ -9,7 +9,12 @@
 
 package org.distorted.play;
 
+import java.io.BufferedWriter;
+import java.io.FileOutputStream;
+import java.io.IOException;
 import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.nio.charset.StandardCharsets;
 
 import android.content.SharedPreferences;
 import android.os.Build;
@@ -24,6 +29,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.library.main.DistortedLibrary;
 import org.distorted.library.type.Static4D;
 import org.distorted.objectlib.main.InitAssets;
@@ -133,6 +139,7 @@ public class PlayActivity extends BaseActivity implements DialogScores.ScoresInv
     protected void onResume() 
       {
       super.onResume();
+
       DistortedLibrary.onResume(ACTIVITY_NUMBER);
       PlayView view = findViewById(R.id.playView);
       ObjectControl control = view.getObjectControl();
@@ -297,6 +304,30 @@ public class PlayActivity extends BaseActivity implements DialogScores.ScoresInv
       return mObjectOrdinal;
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void rememberSolveThread(String name, int level, long time, Static4D rot, int[] quats)
+      {
+      String filename = name+"_solves.json";
+      RubikFiles files = RubikFiles.getInstance();
+      InputStream file = files.openFile(this, filename);
+      RubikRememberedSolves solves = RubikRememberedSolves.getInstance();
+      String contents = solves.addInfo(file,level,time,rot,quats);
+
+      try
+        {
+        FileOutputStream fos = new FileOutputStream(filename);
+        OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8);
+        BufferedWriter bw = new BufferedWriter(osw);
+        bw.write(contents);
+        bw.flush();
+        }
+      catch(IOException ex)
+        {
+        android.util.Log.e("D", "failed to save file "+filename);
+        }
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void rememberSolve()
@@ -312,18 +343,15 @@ public class PlayActivity extends BaseActivity implements DialogScores.ScoresInv
       for(int c=0; c<numCubits; c++) quats[c] = object.getCubitQuatIndex(c);
       Static4D rotQuat = control.getQuat();
 
-      StringBuilder quatStr = new StringBuilder();
-      for(int c=0; c<numCubits; c++)
+      Thread thread = new Thread()
         {
-        quatStr.append(quats[c]);
-        quatStr.append(" ");
-        }
+        public void run()
+          {
+          rememberSolveThread(name,level,time,rotQuat,quats);
+          }
+        };
 
-      android.util.Log.e("D", "object: "+name+"\n"+
-                                    "level: "+level+"\n"+
-                                    "time: "+time+"\n"+
-                                    "rotQuat: " +rotQuat.get0()+" "+rotQuat.get1()+" "+rotQuat.get2()+" "+rotQuat.get3()+"\n"+
-                                    "quats: "+quatStr );
+      thread.start();
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
