commit f0e875148934020811b1f6182e92337a494a89c9
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Mon Mar 16 12:12:19 2020 +0000

    Separate scores from StateSolving

diff --git a/src/main/java/org/distorted/dialog/RubikDialogScoresPagerAdapter.java b/src/main/java/org/distorted/dialog/RubikDialogScoresPagerAdapter.java
index 3fd1b43b..a3dbf133 100644
--- a/src/main/java/org/distorted/dialog/RubikDialogScoresPagerAdapter.java
+++ b/src/main/java/org/distorted/dialog/RubikDialogScoresPagerAdapter.java
@@ -27,7 +27,7 @@ import android.view.View;
 import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
-import org.distorted.network.RubikScoresDownloader;
+import org.distorted.scores.RubikScoresDownloader;
 import org.distorted.object.RubikObjectList;
 
 import static org.distorted.uistate.RubikStatePlay.MAX_SCRAMBLE;
diff --git a/src/main/java/org/distorted/dialog/RubikDialogScoresView.java b/src/main/java/org/distorted/dialog/RubikDialogScoresView.java
index 5bd95873..df107583 100644
--- a/src/main/java/org/distorted/dialog/RubikDialogScoresView.java
+++ b/src/main/java/org/distorted/dialog/RubikDialogScoresView.java
@@ -30,7 +30,7 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import org.distorted.magic.R;
-import static org.distorted.network.RubikScoresDownloader.MAX_PLACES;
+import static org.distorted.scores.RubikScoresDownloader.MAX_PLACES;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/magic/RubikActivity.java b/src/main/java/org/distorted/magic/RubikActivity.java
index 21a36b9c..d2bc5263 100644
--- a/src/main/java/org/distorted/magic/RubikActivity.java
+++ b/src/main/java/org/distorted/magic/RubikActivity.java
@@ -31,7 +31,7 @@ import org.distorted.dialog.RubikDialogSettings;
 import org.distorted.effect.BaseEffect;
 import org.distorted.library.main.DistortedLibrary;
 
-import org.distorted.network.RubikScoresDownloader;
+import org.distorted.scores.RubikScoresDownloader;
 import org.distorted.object.RubikObjectList;
 import org.distorted.uistate.RubikState;
 import org.distorted.uistate.RubikStateAbstract;
diff --git a/src/main/java/org/distorted/network/RubikScoresDownloader.java b/src/main/java/org/distorted/network/RubikScoresDownloader.java
deleted file mode 100644
index 8226528c..00000000
--- a/src/main/java/org/distorted/network/RubikScoresDownloader.java
+++ /dev/null
@@ -1,312 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2019 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.network;
-
-import org.distorted.object.RubikObjectList;
-
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.UnknownHostException;
-
-import static org.distorted.uistate.RubikStatePlay.MAX_SCRAMBLE;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikScoresDownloader implements Runnable
-  {
-  public interface Receiver
-    {
-    void receive(String[][][] country, String[][][] name, String[][][] time);
-    void exception(String exception);
-    }
-
-  public static final int MAX_PLACES = 12;
-
-  private static final int DOWNLOAD   = 0;
-  private static final int SUBMIT     = 1;
-  private static final int IDLE       = 2;
-
-  private static final String URL  ="http://koltunski.pl/rubik/cgi-bin";
-
-  private final String[] hex = {
-    "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
-    "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
-    "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
-    "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
-    "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
-    "%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f",
-    "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
-    "%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f",
-    "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
-    "%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f",
-    "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
-    "%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f",
-    "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
-    "%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f",
-    "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
-    "%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f",
-    "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
-    "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
-    "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
-    "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
-    "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
-    "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
-    "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
-    "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
-    "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
-    "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
-    "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
-    "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
-    "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
-    "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
-    "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
-    "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
-    };
-
-  private static boolean mRunning = false;
-  private static int mMode = IDLE;
-  private static Receiver mReceiver;
-  private static String mUserName, mVersion;
-  private static int mNumRuns;
-
-  private static int mTotal = RubikObjectList.getTotal();
-  private static String mScores = "";
-  private static String[][][] mCountry = new String[mTotal][MAX_SCRAMBLE][MAX_PLACES];
-  private static String[][][] mName    = new String[mTotal][MAX_SCRAMBLE][MAX_PLACES];
-  private static String[][][] mTime    = new String[mTotal][MAX_SCRAMBLE][MAX_PLACES];
-
-  private static int[][] mPlaces = new int[mTotal][MAX_SCRAMBLE];
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void fillValues()
-    {
-    int begin=-1 ,end, len = mScores.length();
-
-    for(int i=0; i<mTotal; i++)
-      for(int j=0; j<MAX_SCRAMBLE; j++)
-        {
-        mPlaces[i][j] = 0;
-        }
-
-    while( begin<len )
-      {
-      end = mScores.indexOf('\n', begin+1);
-      if( end<0 ) end = len;
-      fillRow(mScores.substring(begin+1,end));
-      begin = end;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void fillRow(String row)
-    {
-    int s1 = row.indexOf(' ');
-    int s2 = row.indexOf(' ',s1+1);
-    int s3 = row.indexOf(' ',s2+1);
-    int s4 = row.indexOf(' ',s3+1);
-    int s5 = row.indexOf(' ',s4+1);
-    int s6 = row.length();
-
-    if( s5>s4 && s4>s3 && s3>s2 && s2>s1 && s1>0 )
-      {
-      int size = Integer.parseInt( row.substring(0,s1) );
-
-      if( size>=0 && size<mTotal )
-        {
-        int level      = Integer.parseInt( row.substring(s1+1,s2) );
-        int place      = Integer.parseInt( row.substring(s2+1,s3) );
-        String name    = row.substring(s3+1, s4);
-        int time       = Integer.parseInt( row.substring(s4+1,s5) );
-        String country = row.substring(s5+1, s6);
-        String realTime= String.valueOf(time/10.0f);
-
-        if(level>=0 && level<MAX_SCRAMBLE && place>=0 && place<MAX_PLACES)
-          {
-          int p = mPlaces[size][level];
-          mPlaces[size][level]++;
-
-          if( p!=place ) android.util.Log.e("downloader", "size="+size+" level="+level+" p="+p+" place="+place);
-
-          mCountry[size][level][place] = country;
-          mName   [size][level][place] = name;
-          mTime   [size][level][place] = realTime;
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String URLencode(String s)
-    {
-    StringBuilder sbuf = new StringBuilder();
-    int len = s.length();
-
-    for (int i = 0; i < len; i++)
-      {
-      int ch = s.charAt(i);
-
-           if ('A' <= ch && ch <= 'Z') sbuf.append((char)ch);
-      else if ('a' <= ch && ch <= 'z') sbuf.append((char)ch);
-      else if ('0' <= ch && ch <= '9') sbuf.append((char)ch);
-      else if (ch == ' '             ) sbuf.append('+');
-      else if (ch == '-' || ch == '_'
-            || ch == '.' || ch == '!'
-            || ch == '~' || ch == '*'
-            || ch == '\'' || ch == '('
-            || ch == ')'             ) sbuf.append((char)ch);
-      else if (ch <= 0x007f)           sbuf.append(hex[ch]);
-      else if (ch <= 0x07FF)
-        {
-        sbuf.append(hex[0xc0 | (ch >> 6)]);
-        sbuf.append(hex[0x80 | (ch & 0x3F)]);
-        }
-      else
-        {
-        sbuf.append(hex[0xe0 | (ch >> 12)]);
-        sbuf.append(hex[0x80 | ((ch >> 6) & 0x3F)]);
-        sbuf.append(hex[0x80 | (ch & 0x3F)]);
-        }
-      }
-
-    return sbuf.toString();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean doDownload()
-    {
-    String message=URL+"/download.cgi?n="+URLencode(mUserName)+"&r="+mNumRuns+"&e="+mVersion+"d";
-
-    try
-      {
-      java.net.URL connectURL = new URL(message);
-      HttpURLConnection conn = (HttpURLConnection)connectURL.openConnection();
-
-      conn.setDoInput(true);
-      conn.setDoOutput(true);
-      conn.setUseCaches(false);
-      conn.setRequestMethod("GET");
-      conn.connect();
-      conn.getOutputStream().flush();
-
-      try( InputStream is = conn.getInputStream() )
-        {
-        int ch;
-        StringBuilder sb = new StringBuilder();
-        while( ( ch = is.read() ) != -1 )
-          {
-          sb.append( (char)ch );
-          }
-        mScores = sb.toString();
-        }
-      catch( final Exception e)
-        {
-        mReceiver.exception("Failed to get an answer from the High Scores server");
-        return false;
-        }
-      }
-    catch( final UnknownHostException e )
-      {
-      mReceiver.exception("No access to Internet");
-      return false;
-      }
-    catch( final SecurityException e )
-      {
-      mReceiver.exception("Application not authorized to connect to the Internet");
-      return false;
-      }
-    catch( final Exception e )
-      {
-      mReceiver.exception(e.getMessage());
-      return false;
-      }
-
-    if( mScores.length()==0 )
-      {
-      mReceiver.exception("Failed to download scores");
-      return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean gottaDownload()
-    {
-    return ((mScores.length()==0) && !mRunning);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  @Override
-  public void run()
-    {
-    boolean success=true;
-
-    try
-      {
-      if( gottaDownload() )
-        {
-        mRunning = true;
-        success = doDownload();
-        fillValues();
-        }
-      }
-    catch( Exception e )
-      {
-      mReceiver.exception("Exception downloading records: "+e.getMessage() );
-      }
-
-    mRunning = false;
-
-    if( success )
-      {
-      mReceiver.receive(mCountry, mName, mTime);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void onPause()
-    {
-    mRunning = false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void download(Receiver receiver, String userName, String version, int numRuns)
-    {
-    mReceiver = receiver;
-    mMode     = DOWNLOAD;
-    mUserName = userName;
-    mVersion  = version;
-    mNumRuns  = numRuns;
-
-    Thread networkThrd = new Thread(this);
-    networkThrd.start();
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/scores/RubikScores.java b/src/main/java/org/distorted/scores/RubikScores.java
new file mode 100644
index 00000000..9630946b
--- /dev/null
+++ b/src/main/java/org/distorted/scores/RubikScores.java
@@ -0,0 +1,155 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.scores;
+
+import android.content.SharedPreferences;
+import org.distorted.object.RubikObjectList;
+
+import static org.distorted.object.RubikObjectList.MAX_SIZE;
+import static org.distorted.object.RubikObjectList.NUM_OBJECTS;
+import static org.distorted.uistate.RubikStatePlay.MAX_SCRAMBLE;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// hold my own scores
+
+public class RubikScores
+  {
+  private static final long NO_RECORD = Integer.MAX_VALUE;
+  private long[][][] mRecords;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public RubikScores()
+    {
+    mRecords = new long[NUM_OBJECTS][MAX_SIZE][MAX_SCRAMBLE];
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      for(int j=0; j<MAX_SIZE; j++)
+        for(int k=0; k<MAX_SCRAMBLE; k++)
+          {
+          mRecords[i][j][k] = NO_RECORD;
+          }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+    StringBuilder builder = new StringBuilder();
+    RubikObjectList list;
+    int[] sizes;
+    int length;
+
+    for(int scramble=0; scramble<MAX_SCRAMBLE; scramble++)
+      {
+      builder.setLength(0);
+
+      for(int object=0; object<NUM_OBJECTS; object++)
+        {
+        list = RubikObjectList.getObject(object);
+        sizes = list.getSizes();
+        length = sizes.length;
+
+        for(int size=0; size<length; size++)
+          {
+          builder.append(list.name());
+          builder.append("_");
+          builder.append(sizes[size]);
+          builder.append("=");
+          builder.append(mRecords[object][size][scramble]);
+          builder.append(" ");
+          }
+        }
+
+      editor.putString("record"+scramble, builder.toString());
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+    String recordStr, subStr, nameStr, sizeStr, timeStr;
+    int start, end, equals, underscore;
+    int object, size, time;
+
+    for(int scramble=0; scramble<MAX_SCRAMBLE; scramble++)
+      {
+      start = end = 0;
+      recordStr = preferences.getString("record"+scramble, "");
+
+      //android.util.Log.e("solving", scramble+" record string: "+recordStr);
+
+      while( end!=-1 )
+        {
+        end = recordStr.indexOf(" ", start);
+
+        if( end==-1 ) subStr = recordStr.substring(start);
+        else          subStr = recordStr.substring(start,end);
+
+        start = end+1;
+
+        underscore = subStr.indexOf("_");
+        equals = subStr.indexOf("=");
+
+        if( underscore>=0 && equals>=0 )
+          {
+          nameStr = subStr.substring(0,underscore);
+          sizeStr = subStr.substring(underscore+1, equals);
+          timeStr = subStr.substring(equals+1);
+
+          object = RubikObjectList.getOrdinal(nameStr);
+          size   = RubikObjectList.getSize(object,Integer.parseInt(sizeStr));
+          time   = Integer.parseInt(timeStr);
+
+          if( object>=0 && object< NUM_OBJECTS && size>=0 && size<MAX_SIZE )
+            {
+            mRecords[object][size][scramble] = time;
+
+            if( time<Integer.MAX_VALUE )
+              {
+              android.util.Log.e("solv", "Set record for: object="+object+" size="+size+" scramble="+scramble+" time: "+time);
+              }
+            }
+          else
+            {
+            android.util.Log.e("solv", "error: object="+object+" size="+size);
+            }
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void newRecord(int object, int size, int scramble, long timeTaken)
+    {
+    int maxsize = RubikObjectList.getObject(object).getSizes().length;
+
+    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && scramble>=1 && scramble<=MAX_SCRAMBLE )
+      {
+      if( mRecords[object][size][scramble-1]> timeTaken )
+        {
+        mRecords[object][size][scramble-1] = timeTaken;
+        android.util.Log.e("solv","new record!");
+        }
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/scores/RubikScoresDownloader.java b/src/main/java/org/distorted/scores/RubikScoresDownloader.java
new file mode 100644
index 00000000..1d81e012
--- /dev/null
+++ b/src/main/java/org/distorted/scores/RubikScoresDownloader.java
@@ -0,0 +1,312 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.scores;
+
+import org.distorted.object.RubikObjectList;
+
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.UnknownHostException;
+
+import static org.distorted.uistate.RubikStatePlay.MAX_SCRAMBLE;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikScoresDownloader implements Runnable
+  {
+  public interface Receiver
+    {
+    void receive(String[][][] country, String[][][] name, String[][][] time);
+    void exception(String exception);
+    }
+
+  public static final int MAX_PLACES = 12;
+
+  private static final int DOWNLOAD   = 0;
+  private static final int SUBMIT     = 1;
+  private static final int IDLE       = 2;
+
+  private static final String URL  ="http://koltunski.pl/rubik/cgi-bin";
+
+  private final String[] hex = {
+    "%00", "%01", "%02", "%03", "%04", "%05", "%06", "%07",
+    "%08", "%09", "%0a", "%0b", "%0c", "%0d", "%0e", "%0f",
+    "%10", "%11", "%12", "%13", "%14", "%15", "%16", "%17",
+    "%18", "%19", "%1a", "%1b", "%1c", "%1d", "%1e", "%1f",
+    "%20", "%21", "%22", "%23", "%24", "%25", "%26", "%27",
+    "%28", "%29", "%2a", "%2b", "%2c", "%2d", "%2e", "%2f",
+    "%30", "%31", "%32", "%33", "%34", "%35", "%36", "%37",
+    "%38", "%39", "%3a", "%3b", "%3c", "%3d", "%3e", "%3f",
+    "%40", "%41", "%42", "%43", "%44", "%45", "%46", "%47",
+    "%48", "%49", "%4a", "%4b", "%4c", "%4d", "%4e", "%4f",
+    "%50", "%51", "%52", "%53", "%54", "%55", "%56", "%57",
+    "%58", "%59", "%5a", "%5b", "%5c", "%5d", "%5e", "%5f",
+    "%60", "%61", "%62", "%63", "%64", "%65", "%66", "%67",
+    "%68", "%69", "%6a", "%6b", "%6c", "%6d", "%6e", "%6f",
+    "%70", "%71", "%72", "%73", "%74", "%75", "%76", "%77",
+    "%78", "%79", "%7a", "%7b", "%7c", "%7d", "%7e", "%7f",
+    "%80", "%81", "%82", "%83", "%84", "%85", "%86", "%87",
+    "%88", "%89", "%8a", "%8b", "%8c", "%8d", "%8e", "%8f",
+    "%90", "%91", "%92", "%93", "%94", "%95", "%96", "%97",
+    "%98", "%99", "%9a", "%9b", "%9c", "%9d", "%9e", "%9f",
+    "%a0", "%a1", "%a2", "%a3", "%a4", "%a5", "%a6", "%a7",
+    "%a8", "%a9", "%aa", "%ab", "%ac", "%ad", "%ae", "%af",
+    "%b0", "%b1", "%b2", "%b3", "%b4", "%b5", "%b6", "%b7",
+    "%b8", "%b9", "%ba", "%bb", "%bc", "%bd", "%be", "%bf",
+    "%c0", "%c1", "%c2", "%c3", "%c4", "%c5", "%c6", "%c7",
+    "%c8", "%c9", "%ca", "%cb", "%cc", "%cd", "%ce", "%cf",
+    "%d0", "%d1", "%d2", "%d3", "%d4", "%d5", "%d6", "%d7",
+    "%d8", "%d9", "%da", "%db", "%dc", "%dd", "%de", "%df",
+    "%e0", "%e1", "%e2", "%e3", "%e4", "%e5", "%e6", "%e7",
+    "%e8", "%e9", "%ea", "%eb", "%ec", "%ed", "%ee", "%ef",
+    "%f0", "%f1", "%f2", "%f3", "%f4", "%f5", "%f6", "%f7",
+    "%f8", "%f9", "%fa", "%fb", "%fc", "%fd", "%fe", "%ff"
+    };
+
+  private static boolean mRunning = false;
+  private static int mMode = IDLE;
+  private static Receiver mReceiver;
+  private static String mUserName, mVersion;
+  private static int mNumRuns;
+
+  private static int mTotal = RubikObjectList.getTotal();
+  private static String mScores = "";
+  private static String[][][] mCountry = new String[mTotal][MAX_SCRAMBLE][MAX_PLACES];
+  private static String[][][] mName    = new String[mTotal][MAX_SCRAMBLE][MAX_PLACES];
+  private static String[][][] mTime    = new String[mTotal][MAX_SCRAMBLE][MAX_PLACES];
+
+  private static int[][] mPlaces = new int[mTotal][MAX_SCRAMBLE];
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void fillValues()
+    {
+    int begin=-1 ,end, len = mScores.length();
+
+    for(int i=0; i<mTotal; i++)
+      for(int j=0; j<MAX_SCRAMBLE; j++)
+        {
+        mPlaces[i][j] = 0;
+        }
+
+    while( begin<len )
+      {
+      end = mScores.indexOf('\n', begin+1);
+      if( end<0 ) end = len;
+      fillRow(mScores.substring(begin+1,end));
+      begin = end;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void fillRow(String row)
+    {
+    int s1 = row.indexOf(' ');
+    int s2 = row.indexOf(' ',s1+1);
+    int s3 = row.indexOf(' ',s2+1);
+    int s4 = row.indexOf(' ',s3+1);
+    int s5 = row.indexOf(' ',s4+1);
+    int s6 = row.length();
+
+    if( s5>s4 && s4>s3 && s3>s2 && s2>s1 && s1>0 )
+      {
+      int size = Integer.parseInt( row.substring(0,s1) );
+
+      if( size>=0 && size<mTotal )
+        {
+        int level      = Integer.parseInt( row.substring(s1+1,s2) );
+        int place      = Integer.parseInt( row.substring(s2+1,s3) );
+        String name    = row.substring(s3+1, s4);
+        int time       = Integer.parseInt( row.substring(s4+1,s5) );
+        String country = row.substring(s5+1, s6);
+        String realTime= String.valueOf(time/10.0f);
+
+        if(level>=0 && level<MAX_SCRAMBLE && place>=0 && place<MAX_PLACES)
+          {
+          int p = mPlaces[size][level];
+          mPlaces[size][level]++;
+
+          if( p!=place ) android.util.Log.e("downloader", "size="+size+" level="+level+" p="+p+" place="+place);
+
+          mCountry[size][level][place] = country;
+          mName   [size][level][place] = name;
+          mTime   [size][level][place] = realTime;
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String URLencode(String s)
+    {
+    StringBuilder sbuf = new StringBuilder();
+    int len = s.length();
+
+    for (int i = 0; i < len; i++)
+      {
+      int ch = s.charAt(i);
+
+           if ('A' <= ch && ch <= 'Z') sbuf.append((char)ch);
+      else if ('a' <= ch && ch <= 'z') sbuf.append((char)ch);
+      else if ('0' <= ch && ch <= '9') sbuf.append((char)ch);
+      else if (ch == ' '             ) sbuf.append('+');
+      else if (ch == '-' || ch == '_'
+            || ch == '.' || ch == '!'
+            || ch == '~' || ch == '*'
+            || ch == '\'' || ch == '('
+            || ch == ')'             ) sbuf.append((char)ch);
+      else if (ch <= 0x007f)           sbuf.append(hex[ch]);
+      else if (ch <= 0x07FF)
+        {
+        sbuf.append(hex[0xc0 | (ch >> 6)]);
+        sbuf.append(hex[0x80 | (ch & 0x3F)]);
+        }
+      else
+        {
+        sbuf.append(hex[0xe0 | (ch >> 12)]);
+        sbuf.append(hex[0x80 | ((ch >> 6) & 0x3F)]);
+        sbuf.append(hex[0x80 | (ch & 0x3F)]);
+        }
+      }
+
+    return sbuf.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean doDownload()
+    {
+    String message=URL+"/download.cgi?n="+URLencode(mUserName)+"&r="+mNumRuns+"&e="+mVersion+"d";
+
+    try
+      {
+      java.net.URL connectURL = new URL(message);
+      HttpURLConnection conn = (HttpURLConnection)connectURL.openConnection();
+
+      conn.setDoInput(true);
+      conn.setDoOutput(true);
+      conn.setUseCaches(false);
+      conn.setRequestMethod("GET");
+      conn.connect();
+      conn.getOutputStream().flush();
+
+      try( InputStream is = conn.getInputStream() )
+        {
+        int ch;
+        StringBuilder sb = new StringBuilder();
+        while( ( ch = is.read() ) != -1 )
+          {
+          sb.append( (char)ch );
+          }
+        mScores = sb.toString();
+        }
+      catch( final Exception e)
+        {
+        mReceiver.exception("Failed to get an answer from the High Scores server");
+        return false;
+        }
+      }
+    catch( final UnknownHostException e )
+      {
+      mReceiver.exception("No access to Internet");
+      return false;
+      }
+    catch( final SecurityException e )
+      {
+      mReceiver.exception("Application not authorized to connect to the Internet");
+      return false;
+      }
+    catch( final Exception e )
+      {
+      mReceiver.exception(e.getMessage());
+      return false;
+      }
+
+    if( mScores.length()==0 )
+      {
+      mReceiver.exception("Failed to download scores");
+      return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean gottaDownload()
+    {
+    return ((mScores.length()==0) && !mRunning);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Override
+  public void run()
+    {
+    boolean success=true;
+
+    try
+      {
+      if( gottaDownload() )
+        {
+        mRunning = true;
+        success = doDownload();
+        fillValues();
+        }
+      }
+    catch( Exception e )
+      {
+      mReceiver.exception("Exception downloading records: "+e.getMessage() );
+      }
+
+    mRunning = false;
+
+    if( success )
+      {
+      mReceiver.receive(mCountry, mName, mTime);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void onPause()
+    {
+    mRunning = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void download(Receiver receiver, String userName, String version, int numRuns)
+    {
+    mReceiver = receiver;
+    mMode     = DOWNLOAD;
+    mUserName = userName;
+    mVersion  = version;
+    mNumRuns  = numRuns;
+
+    Thread networkThrd = new Thread(this);
+    networkThrd.start();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/uistate/RubikStateSolving.java b/src/main/java/org/distorted/uistate/RubikStateSolving.java
index e2dbab24..71c00610 100644
--- a/src/main/java/org/distorted/uistate/RubikStateSolving.java
+++ b/src/main/java/org/distorted/uistate/RubikStateSolving.java
@@ -28,41 +28,26 @@ import android.widget.TextView;
 
 import org.distorted.magic.R;
 import org.distorted.magic.RubikActivity;
-import org.distorted.object.RubikObjectList;
+import org.distorted.scores.RubikScores;
 
 import java.util.Timer;
 import java.util.TimerTask;
 
-import static android.view.View.INVISIBLE;
-import static org.distorted.object.RubikObjectList.NUM_OBJECTS;
-import static org.distorted.object.RubikObjectList.MAX_SIZE;
-import static org.distorted.uistate.RubikStatePlay.MAX_SCRAMBLE;
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
 public class RubikStateSolving extends RubikStateAbstract
   {
-  private static final long NO_RECORD = Integer.MAX_VALUE;
-
   private TextView mTime;
   private Timer mTimer;
   private long mStartTime;
   private boolean mRunning;
-
-  private long[][][] mRecords;
+  private RubikScores mScores;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   RubikStateSolving()
     {
-    mRecords = new long[NUM_OBJECTS][MAX_SIZE][MAX_SCRAMBLE];
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      for(int j=0; j<MAX_SIZE; j++)
-        for(int k=0; k<MAX_SCRAMBLE; k++)
-          {
-          mRecords[i][j][k] = NO_RECORD;
-          }
+    mScores = new RubikScores();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -111,96 +96,21 @@ public class RubikStateSolving extends RubikStateAbstract
     buttonR.setOnClickListener(act);
     layoutBot.addView(buttonR);
 
-    buttonL.setVisibility(INVISIBLE);
+    buttonL.setVisibility(android.view.View.INVISIBLE);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void savePreferences(SharedPreferences.Editor editor)
     {
-    StringBuilder builder = new StringBuilder();
-    RubikObjectList list;
-    int[] sizes;
-    int length;
-
-    for(int scramble=0; scramble<MAX_SCRAMBLE; scramble++)
-      {
-      builder.setLength(0);
-
-      for(int object=0; object<NUM_OBJECTS; object++)
-        {
-        list = RubikObjectList.getObject(object);
-        sizes = list.getSizes();
-        length = sizes.length;
-
-        for(int size=0; size<length; size++)
-          {
-          builder.append(list.name());
-          builder.append("_");
-          builder.append(sizes[size]);
-          builder.append("=");
-          builder.append(mRecords[object][size][scramble]);
-          builder.append(" ");
-          }
-        }
-
-      editor.putString("record"+scramble, builder.toString());
-      }
+    mScores.savePreferences(editor);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void restorePreferences(SharedPreferences preferences)
     {
-    String recordStr, subStr, nameStr, sizeStr, timeStr;
-    int start, end, equals, underscore;
-    int object, size, time;
-
-    for(int scramble=0; scramble<MAX_SCRAMBLE; scramble++)
-      {
-      start = end = 0;
-      recordStr = preferences.getString("record"+scramble, "");
-
-      //android.util.Log.e("solving", scramble+" record string: "+recordStr);
-
-      while( end!=-1 )
-        {
-        end = recordStr.indexOf(" ", start);
-
-        if( end==-1 ) subStr = recordStr.substring(start);
-        else          subStr = recordStr.substring(start,end);
-
-        start = end+1;
-
-        underscore = subStr.indexOf("_");
-        equals = subStr.indexOf("=");
-
-        if( underscore>=0 && equals>=0 )
-          {
-          nameStr = subStr.substring(0,underscore);
-          sizeStr = subStr.substring(underscore+1, equals);
-          timeStr = subStr.substring(equals+1);
-
-          object = RubikObjectList.getOrdinal(nameStr);
-          size   = RubikObjectList.getSize(object,Integer.parseInt(sizeStr));
-          time   = Integer.parseInt(timeStr);
-
-          if( object>=0 && object< NUM_OBJECTS && size>=0 && size<MAX_SIZE )
-            {
-            mRecords[object][size][scramble] = time;
-
-            if( time<Integer.MAX_VALUE )
-              {
-              android.util.Log.e("solv", "Set record for: object="+object+" size="+size+" scramble="+scramble+" time: "+time);
-              }
-            }
-          else
-            {
-            android.util.Log.e("solv", "error: object="+object+" size="+size);
-            }
-          }
-        }
-      }
+    mScores.restorePreferences(preferences);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -251,17 +161,8 @@ public class RubikStateSolving extends RubikStateAbstract
       int object  = play.getObject();
       int size    = play.getSize();
       int scramble= play.getPicker();
-      int maxsize = RubikObjectList.getObject(object).getSizes().length;
-
-      if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && scramble>=1 && scramble<=MAX_SCRAMBLE )
-        {
-        if( mRecords[object][size][scramble-1]> timeTaken )
-          {
-          mRecords[object][size][scramble-1] = timeTaken;
-          android.util.Log.e("solv","new record!");
-          }
-        }
 
+      mScores.newRecord(object, size, scramble, timeTaken);
       return timeTaken;
       }
 
