commit 6a083c6a33885e6eb88aae4fd5d996ceb5a00709
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Apr 23 14:18:33 2021 +0200

    - report the Graphics driver's Renderer and Version.
    - new Diamond, Skewb2 and Skewb3 meshes.

diff --git a/src/main/java/org/distorted/dialogs/RubikDialogAbout.java b/src/main/java/org/distorted/dialogs/RubikDialogAbout.java
index 8f383ec9..0ee8aef1 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogAbout.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogAbout.java
@@ -36,7 +36,6 @@ import android.util.TypedValue;
 import android.view.LayoutInflater;
 import android.view.View;
 import android.view.Window;
-import android.view.WindowManager;
 import android.widget.Button;
 import android.widget.TextView;
 
@@ -104,6 +103,11 @@ public class RubikDialogAbout extends AppCompatDialogFragment
     else
       {
       text4.setVisibility(View.GONE);
+      /*
+      text4.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+      String version = DistortedLibrary.getDriverVersion();
+      text4.setText(version);
+      */
       }
 
     builder.setView(view);
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
index 427901ed..cb015c2f 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
@@ -38,7 +38,7 @@ import android.widget.TextView;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 import org.distorted.states.StateList;
 import org.distorted.states.RubikStatePlay;
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
index 643f9c79..2fb77cb8 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
@@ -31,14 +31,14 @@ import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
 import org.distorted.main.R;
-import org.distorted.scores.RubikScores;
-import org.distorted.scores.RubikScoresDownloader;
+import org.distorted.network.RubikScores;
+import org.distorted.network.RubikNetwork;
 import org.distorted.objects.ObjectList;
 import org.distorted.states.RubikStatePlay;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-class RubikDialogScoresPagerAdapter extends PagerAdapter implements RubikScoresDownloader.Receiver
+class RubikDialogScoresPagerAdapter extends PagerAdapter implements RubikNetwork.Receiver
   {
   private final FragmentActivity mAct;
   private final RubikDialogScores mDialog;
@@ -242,10 +242,10 @@ class RubikDialogScoresPagerAdapter extends PagerAdapter implements RubikScoresD
 
     if( allCreated )
       {
-      RubikScoresDownloader downloader = RubikScoresDownloader.getInstance();
+      RubikNetwork network = RubikNetwork.getInstance();
 
-      if( mIsSubmitting )  downloader.submit  ( this, mAct );
-      else                 downloader.download( this, mAct );
+      if( mIsSubmitting )  network.submit  ( this, mAct );
+      else                 network.download( this, mAct );
       }
 
     return mViews[position];
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
index 1fe5263d..e28b7120 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
@@ -34,9 +34,9 @@ import android.widget.TextView;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 
-import static org.distorted.scores.RubikScoresDownloader.MAX_PLACES;
+import static org.distorted.network.RubikNetwork.MAX_PLACES;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
index ef1bf5f8..8dc254f3 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
@@ -41,7 +41,7 @@ import android.widget.TextView;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 import org.distorted.states.StateList;
 import org.distorted.states.RubikStatePlay;
 
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index f32fbd61..7998e000 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -42,8 +42,8 @@ import org.distorted.effects.BaseEffect;
 import org.distorted.library.main.DistortedLibrary;
 
 import org.distorted.objects.TwistyObject;
-import org.distorted.scores.RubikScores;
-import org.distorted.scores.RubikScoresDownloader;
+import org.distorted.network.RubikScores;
+import org.distorted.network.RubikNetwork;
 import org.distorted.objects.ObjectList;
 import org.distorted.states.StateList;
 import org.distorted.states.RubikStatePlay;
@@ -227,7 +227,7 @@ public class RubikActivity extends AppCompatActivity
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       view.onPause();
       DistortedLibrary.onPause(0);
-      RubikScoresDownloader.onPause();
+      RubikNetwork.onPause();
       savePreferences();
       }
 
diff --git a/src/main/java/org/distorted/main/RubikPreRender.java b/src/main/java/org/distorted/main/RubikPreRender.java
index 2a5f9f07..75f89125 100644
--- a/src/main/java/org/distorted/main/RubikPreRender.java
+++ b/src/main/java/org/distorted/main/RubikPreRender.java
@@ -41,7 +41,7 @@ import org.distorted.effects.EffectController;
 import org.distorted.effects.scramble.ScrambleEffect;
 import org.distorted.objects.TwistyObject;
 import org.distorted.objects.ObjectList;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 import org.distorted.states.RubikStatePlay;
 import org.distorted.states.StateList;
 import org.distorted.states.RubikStateSolving;
diff --git a/src/main/java/org/distorted/main/RubikRenderer.java b/src/main/java/org/distorted/main/RubikRenderer.java
index e35fbba0..e35d64f7 100644
--- a/src/main/java/org/distorted/main/RubikRenderer.java
+++ b/src/main/java/org/distorted/main/RubikRenderer.java
@@ -29,6 +29,7 @@ import org.distorted.library.effect.VertexEffectRotate;
 import org.distorted.library.main.DistortedLibrary;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.mesh.MeshBase;
+import org.distorted.network.RubikNetwork;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
@@ -45,6 +46,7 @@ public class RubikRenderer implements GLSurfaceView.Renderer, DistortedLibrary.E
    private final DistortedScreen mScreen;
    private final Fps mFPS;
    private boolean mErrorShown;
+   private boolean mDebugSent;
 
    private static class Fps
      {
@@ -131,6 +133,13 @@ public class RubikRenderer implements GLSurfaceView.Renderer, DistortedLibrary.E
       BaseEffect.Type.enableEffects();
 
       DistortedLibrary.onSurfaceCreated(mView.getContext(),this,1);
+
+      if( !mDebugSent )
+        {
+        mDebugSent= true;
+        RubikNetwork network = RubikNetwork.getInstance();
+        network.debug( (RubikActivity)mView.getContext());
+        }
       }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/network/RubikNetwork.java b/src/main/java/org/distorted/network/RubikNetwork.java
new file mode 100644
index 00000000..66424db4
--- /dev/null
+++ b/src/main/java/org/distorted/network/RubikNetwork.java
@@ -0,0 +1,562 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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 android.app.Activity;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+
+import androidx.appcompat.app.AppCompatActivity;
+import androidx.fragment.app.FragmentActivity;
+
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.objects.ObjectList;
+
+import java.io.InputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import static org.distorted.objects.ObjectList.MAX_LEVEL;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikNetwork implements Runnable
+  {
+  public interface Receiver
+    {
+    void receive(String[][][] country, String[][][] name, float[][][] time);
+    void message(String mess);
+    void error(String error);
+    }
+
+  public static final int MAX_PLACES = 10;
+
+  private static final int DOWNLOAD   = 0;
+  private static final int SUBMIT     = 1;
+  private static final int DEBUG      = 2;
+  private static final int IDLE       = 3;
+
+  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 final int mTotal = ObjectList.getTotal();
+  private static final String[][][] mCountry = new String[mTotal][MAX_LEVEL][MAX_PLACES];
+  private static final String[][][] mName    = new String[mTotal][MAX_LEVEL][MAX_PLACES];
+  private static final  float[][][] mTime    = new  float[mTotal][MAX_LEVEL][MAX_PLACES];
+  private static final int[][] mPlaces = new int[mTotal][MAX_LEVEL];
+
+  private static RubikNetwork mThis;
+  private static String mScores = "";
+  private static boolean mRunning = false;
+  private static int mMode = IDLE;
+  private static Receiver mReceiver;
+  private static String mVersion;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static String computeHash(String stringToHash, byte[] salt)
+    {
+    String generatedPassword;
+
+    try
+      {
+      MessageDigest md = MessageDigest.getInstance("MD5");
+      md.update(salt);
+      byte[] bytes = md.digest(stringToHash.getBytes());
+      StringBuilder sb = new StringBuilder();
+
+      for (byte aByte : bytes)
+        {
+        sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
+        }
+
+      generatedPassword = sb.toString();
+      }
+    catch (NoSuchAlgorithmException e)
+      {
+      return "NoSuchAlgorithm";
+      }
+
+    return generatedPassword;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean fillValues()
+    {
+    int begin=-1 ,end, len = mScores.length();
+    String row;
+
+    if( len==0 )
+      {
+      mReceiver.error("1");
+      return false;
+      }
+    else if( len<=2 )
+      {
+      mReceiver.error(mScores);
+      return false;
+      }
+
+    for(int i=0; i<mTotal; i++)
+      for(int j=0; j<MAX_LEVEL; j++)
+        {
+        mPlaces[i][j] = 0;
+        }
+
+    while( begin<len )
+      {
+      end = mScores.indexOf('\n', begin+1);
+      if( end<0 ) end = len;
+
+      try
+        {
+        row = mScores.substring(begin+1,end);
+        fillRow(row);
+        }
+      catch(Exception ex)
+        {
+        // faulty row - ignore
+        }
+
+      begin = end;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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.length();
+
+    if( s5>s4 && s4>s3 && s3>s2 && s2>s1 && s1>0 )
+      {
+      int object = ObjectList.unpackObjectFromString( row.substring(0,s1) );
+
+      if( object>=0 && object<mTotal )
+        {
+        int level      = Integer.parseInt( row.substring(s1+1,s2) );
+        String name    = row.substring(s2+1, s3);
+        int time       = Integer.parseInt( row.substring(s3+1,s4) );
+        String country = row.substring(s4+1, s5);
+
+        if( country.equals("do") ) country = "dm"; // see RubikScores.setCountry()
+
+        if(level>=0 && level<MAX_LEVEL)
+          {
+          int p = mPlaces[object][level];
+          mPlaces[object][level]++;
+
+          mCountry[object][level][p] = country;
+          mName   [object][level][p] = name;
+          mTime   [object][level][p] = ((float)(time/10))/100.0f;
+          }
+        }
+      }
+    else
+      {
+      tryDoCommand(row);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void tryDoCommand(String row)
+    {
+    if( row.startsWith("comm") )
+      {
+      int colon = row.indexOf(':');
+
+      if( colon>0 )
+        {
+        String commandNumber = row.substring(4,colon);
+        int number;
+
+        try
+          {
+          number = Integer.parseInt(commandNumber);
+          }
+        catch(NumberFormatException ex)
+          {
+          number=0;
+          }
+
+        if(number==1)
+          {
+          String country = row.substring(colon+1);
+          RubikScores scores = RubikScores.getInstance();
+          scores.setCountry(country);
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 void sendDebug()
+    {
+    String url = constructDebugURL();
+
+    try
+      {
+      java.net.URL connectURL = new URL(url);
+      HttpURLConnection conn = (HttpURLConnection)connectURL.openConnection();
+
+      conn.setDoInput(true);
+      conn.setDoOutput(true);
+      conn.setUseCaches(false);
+      conn.setRequestMethod("GET");
+      conn.connect();
+      conn.getOutputStream().flush();
+      conn.getInputStream();
+      }
+    catch( final Exception e )
+      {
+      // ignore
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean network(String url)
+    {
+    try
+      {
+      java.net.URL connectURL = new URL(url);
+      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.message("Failed to get an answer from the High Scores server");
+        return false;
+        }
+      }
+    catch( final UnknownHostException e )
+      {
+      mReceiver.message("No access to Internet");
+      return false;
+      }
+    catch( final SecurityException e )
+      {
+      mReceiver.message("Application not authorized to connect to the Internet");
+      return false;
+      }
+    catch( final Exception e )
+      {
+      mReceiver.message(e.getMessage());
+      return false;
+      }
+
+    if( mScores.length()==0 )
+      {
+      mReceiver.message("Failed to download scores");
+      return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String constructDebugURL()
+    {
+    RubikScores scores = RubikScores.getInstance();
+    String name = URLencode(scores.getName());
+    int numRuns = scores.getNumRuns();
+    int numPlay = scores.getNumPlays();
+    String country = scores.getCountry();
+    String renderer = DistortedLibrary.getDriverRenderer();
+    String version  = DistortedLibrary.getDriverVersion();
+
+    renderer = URLencode(renderer);
+    version  = URLencode(version);
+
+    String url="https://distorted.org/magic/cgi-bin/debugs.cgi";
+    url += "?n="+name+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion+"d"+"&d="+renderer+"&v="+version;
+
+    return url;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String constructDownloadURL()
+    {
+    RubikScores scores = RubikScores.getInstance();
+    String name = URLencode(scores.getName());
+    String veri = scores.isVerified() ? name : "";
+    int numRuns = scores.getNumRuns();
+    int numPlay = scores.getNumPlays();
+    String country = scores.getCountry();
+
+    String url="https://distorted.org/magic/cgi-bin/download.cgi";
+    url += "?n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion+"d";
+    url += "&o="+ ObjectList.getObjectList()+"&min=0&max="+MAX_LEVEL+"&l="+MAX_PLACES;
+
+    return url;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String constructSubmitURL()
+    {
+    RubikScores scores = RubikScores.getInstance();
+    String name = URLencode(scores.getName());
+    String veri = scores.isVerified() ? name : "";
+    int numRuns = scores.getNumRuns();
+    int numPlay = scores.getNumPlays();
+    int deviceID= scores.getDeviceID();
+    String reclist = scores.getRecordList("&o=","&l=","&t=");
+    String country = scores.getCountry();
+    long epoch = System.currentTimeMillis();
+    String salt = "cuboid";
+
+    String url1="https://distorted.org/magic/cgi-bin/submit.cgi";
+    String url2 = "n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&i="+deviceID+"&e="+mVersion+"d";
+    url2 += reclist+"&c="+country+"&f="+epoch+"&oo="+ ObjectList.getObjectList();
+    url2 += "&min=0&max="+MAX_LEVEL+"&lo="+MAX_PLACES;
+    String hash = computeHash( url2, salt.getBytes() );
+
+    return url1 + "?" + url2 + "&h=" + hash;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean gottaDownload()
+    {
+    return ((mScores.length()==0) && !mRunning);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  @Override
+  public void run()
+    {
+    boolean receiveValues=true;
+
+    try
+      {
+      if( mMode==DOWNLOAD && gottaDownload() )
+        {
+        mRunning = true;
+        receiveValues = network(constructDownloadURL());
+        }
+      if( mMode==SUBMIT )
+        {
+        mRunning = true;
+
+        if( RubikScores.getInstance().thereAreUnsubmittedRecords() )
+          {
+          receiveValues = network(constructSubmitURL());
+          }
+        }
+      if( mMode==DEBUG )
+        {
+        sendDebug();
+        receiveValues = false;
+        mRunning = false;
+        }
+      }
+    catch( Exception e )
+      {
+      if( mReceiver!=null ) mReceiver.message("Exception downloading records: "+e.getMessage() );
+      }
+
+    if( mRunning )
+      {
+      receiveValues = fillValues();
+      mRunning = false;
+      }
+
+    if( receiveValues )
+      {
+      if( mReceiver!=null ) mReceiver.receive(mCountry, mName, mTime);
+
+      if( mMode==SUBMIT )
+        {
+        RubikScores.getInstance().successfulSubmit();
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RubikNetwork()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void onPause()
+    {
+    mRunning = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static RubikNetwork getInstance()
+    {
+    if( mThis==null )
+      {
+      mThis = new RubikNetwork();
+      }
+
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void start(Receiver receiver, Activity act, int mode)
+    {
+    mReceiver = receiver;
+    mMode     = mode;
+
+    try
+      {
+      PackageInfo pInfo = act.getPackageManager().getPackageInfo( act.getPackageName(), 0);
+      mVersion = pInfo.versionName;
+      }
+    catch (PackageManager.NameNotFoundException e)
+      {
+      mVersion = "0.9.2";
+      }
+
+    Thread networkThrd = new Thread(this);
+    networkThrd.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void download(Receiver receiver, FragmentActivity act)
+    {
+    start(receiver, act, DOWNLOAD);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void submit(Receiver receiver, FragmentActivity act)
+    {
+    start(receiver, act, SUBMIT);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void debug(AppCompatActivity act)
+    {
+    start(null, act, DEBUG);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/network/RubikScores.java b/src/main/java/org/distorted/network/RubikScores.java
new file mode 100644
index 00000000..a0e951b3
--- /dev/null
+++ b/src/main/java/org/distorted/network/RubikScores.java
@@ -0,0 +1,500 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.network;
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.telephony.TelephonyManager;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.main.BuildConfig;
+import org.distorted.objects.ObjectList;
+
+import java.util.UUID;
+
+import static org.distorted.objects.ObjectList.MAX_NUM_OBJECTS;
+import static org.distorted.objects.ObjectList.NUM_OBJECTS;
+import static org.distorted.objects.ObjectList.MAX_LEVEL;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// hold my own scores, and some other statistics.
+
+public class RubikScores
+  {
+  public static final long NO_RECORD = Long.MAX_VALUE;
+  private static RubikScores mThis;
+
+  private final long[][][] mRecords;
+  private final int [][][] mSubmitted;
+
+  private String mName, mCountry;
+  private boolean mNameIsVerified;
+  private int mNumRuns;
+  private int mNumPlays;
+  private int mNumWins;
+  private int mDeviceID;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RubikScores()
+    {
+    mRecords   = new long[NUM_OBJECTS][MAX_NUM_OBJECTS][MAX_LEVEL];
+    mSubmitted = new int [NUM_OBJECTS][MAX_NUM_OBJECTS][MAX_LEVEL];
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      for(int j=0; j<MAX_NUM_OBJECTS; j++)
+        for(int k=0; k<MAX_LEVEL; k++)
+          {
+          mRecords[i][j][k]   = NO_RECORD;
+          mSubmitted[i][j][k] = 0;
+          }
+
+    mName = "";
+    mCountry = "un";
+
+    mNameIsVerified = false;
+
+    mNumPlays= -1;
+    mNumRuns = -1;
+    mDeviceID= -1;
+    mNumWins =  0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int privateGetDeviceID()
+    {
+    int id;
+
+    try
+      {
+      String s = UUID.randomUUID().toString();
+      id = s.hashCode();
+      }
+    catch(Exception ex)
+      {
+      id = 0;
+      android.util.Log.e("scores", "Exception in getDeviceID()");
+      }
+
+    return id<0 ? -id : id;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized void successfulSubmit()
+    {
+    mNameIsVerified = true;
+
+    for(int i=0; i<NUM_OBJECTS; i++)
+      for(int j=0; j<MAX_NUM_OBJECTS; j++)
+        for(int k=0; k<MAX_LEVEL; k++)
+          {
+          mSubmitted[i][j][k]=1;
+          }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getDeviceID()
+    {
+    return mDeviceID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized boolean thereAreUnsubmittedRecords()
+    {
+    ObjectList list;
+    int length;
+
+    for(int object=0; object<NUM_OBJECTS; object++)
+      {
+      list = ObjectList.getObject(object);
+      length = list.getSizes().length;
+
+      for(int size=0; size<length; size++)
+        for(int level=0; level<MAX_LEVEL; level++)
+          {
+          if( mSubmitted[object][size][level]==0 && mRecords[object][size][level]<NO_RECORD )
+            {
+            return true;
+            }
+          }
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized String getRecordList(String strObj, String strLvl, String strTim)
+    {
+    ObjectList list;
+    StringBuilder builderObj = new StringBuilder();
+    StringBuilder builderLvl = new StringBuilder();
+    StringBuilder builderTim = new StringBuilder();
+    boolean first = true;
+    int[] sizes;
+    int length;
+
+    for(int object=0; object<NUM_OBJECTS; object++)
+      {
+      list = ObjectList.getObject(object);
+      sizes = list.getSizes();
+      length = sizes.length;
+
+      for(int size=0; size<length; size++)
+        {
+        for(int level=0; level<MAX_LEVEL; level++)
+          {
+          if( mSubmitted[object][size][level]==0 && mRecords[object][size][level]<NO_RECORD )
+            {
+            if( !first )
+              {
+              builderObj.append(',');
+              builderLvl.append(',');
+              builderTim.append(',');
+              }
+            else
+              {
+              first=false;
+              }
+
+            builderObj.append(list.name());
+            builderObj.append("_");
+            builderObj.append(sizes[size]);
+            builderLvl.append(level);
+            builderTim.append(mRecords[object][size][level]);
+            }
+          }
+        }
+      }
+
+    return strObj+builderObj.toString()+strLvl+builderLvl.toString()+strTim+builderTim.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Public API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized long getRecord(int object, int size, int level)
+    {
+    int maxsize = ObjectList.getObject(object).getSizes().length;
+
+    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
+      {
+      return mRecords[object][size][level];
+      }
+
+    return -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized boolean isSubmitted(int object, int size, int level)
+    {
+    int maxsize = ObjectList.getObject(object).getSizes().length;
+
+    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
+      {
+      return mSubmitted[object][size][level]==1;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isVerified()
+    {
+    return mNameIsVerified;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumPlays()
+    {
+    return mNumPlays;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getNumRuns()
+    {
+    return mNumRuns;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getName()
+    {
+    return mName;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getCountry()
+    {
+    return mCountry;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void incrementNumPlays()
+    {
+    mNumPlays++;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void incrementNumRuns()
+    {
+    mNumRuns++;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int incrementNumWins()
+    {
+    mNumWins++;
+    return mNumWins;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setName(String newName)
+    {
+    mName = newName;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized boolean isSolved(int object, int size, int level)
+    {
+    int maxsize = ObjectList.getObject(object).getSizes().length;
+
+    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
+      {
+      return mRecords[object][size][level]<NO_RECORD;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setCountry(Context context)
+    {
+    TelephonyManager tM =((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
+
+    if( tM!=null )
+      {
+      mCountry = tM.getSimCountryIso();
+
+      if( mCountry==null || mCountry.length()<=1 )
+        {
+        mCountry = tM.getNetworkCountryIso();
+        }
+      }
+
+    // Special case: Dominicana. Its ISO-3166-alpha-2 country code is 'do' which we can't have here
+    // because we later on map this to a resource name (the flag) and 'do' is a reserved Java keyword
+    // and can't be a resource name.
+
+    if( mCountry.equals("do") ) mCountry = "dm";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setCountry(String country)
+    {
+    mCountry = country;
+
+    if( mCountry.equals("do") ) mCountry = "dm";  // see above
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static RubikScores getInstance()
+    {
+    if( mThis==null )
+      {
+      mThis = new RubikScores();
+      }
+
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void savePreferences(SharedPreferences.Editor editor)
+    {
+    StringBuilder builder = new StringBuilder();
+    ObjectList list;
+    int[] sizes;
+    int length;
+
+    for(int level=0; level<MAX_LEVEL; level++)
+      {
+      builder.setLength(0);
+
+      for(int object=0; object<NUM_OBJECTS; object++)
+        {
+        list = ObjectList.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][level]);
+          builder.append(",");
+          builder.append(mSubmitted[object][size][level]);
+          builder.append(" ");
+          }
+        }
+
+      editor.putString("scores_record"+level, builder.toString());
+      }
+
+    editor.putString("scores_name"  , mName  );
+    editor.putBoolean("scores_isVerified", mNameIsVerified);
+    editor.putInt("scores_numPlays", mNumPlays);
+    editor.putInt("scores_numRuns" , mNumRuns );
+    editor.putInt("scores_deviceid", mDeviceID);
+    editor.putInt("scores_review"  , mNumWins );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized void restorePreferences(SharedPreferences preferences)
+    {
+    String recordStr, subStr, nameStr, sizeStr, timeStr, submStr, errorStr="";
+    int start, end, equals, underscore, comma;
+    int object, sizeIndex, subm;
+    long time;
+    boolean thereWasError = false;
+
+    for(int level=0; level<MAX_LEVEL; level++)
+      {
+      start = end = 0;
+      recordStr = preferences.getString("scores_record"+level, "");
+
+      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("=");
+        comma = subStr.indexOf(",");
+
+        if( underscore>=0 && equals>=0 && comma>=0 )
+          {
+          nameStr = subStr.substring(0,underscore);
+          sizeStr = subStr.substring(underscore+1, equals);
+          timeStr = subStr.substring(equals+1,comma);
+          submStr = subStr.substring(comma+1);
+
+          object = ObjectList.getOrdinal(nameStr);
+
+          if( object>=0 && object< NUM_OBJECTS )
+            {
+            sizeIndex = ObjectList.getSizeIndex(object,Integer.parseInt(sizeStr));
+            time = Long.parseLong(timeStr);
+            subm = Integer.parseInt(submStr);
+
+            if( sizeIndex>=0 && sizeIndex<MAX_NUM_OBJECTS && subm>=0 && subm<=1 )
+              {
+              mRecords  [object][sizeIndex][level] = time;
+              mSubmitted[object][sizeIndex][level] = subm;
+              }
+            else
+              {
+              errorStr += ("error1: size="+sizeIndex+" subm="+subm+" obj: "+nameStr+" size: "+sizeStr+"\n");
+              thereWasError= true;
+              }
+            }
+          else
+            {
+            errorStr += ("error2: object="+object+" obj: "+nameStr+" size: "+sizeStr+"\n");
+            thereWasError = true;
+            }
+          }
+        }
+      }
+
+    mName           = preferences.getString("scores_name"  , "" );
+    mNameIsVerified = preferences.getBoolean("scores_isVerified", false);
+    mNumPlays       = preferences.getInt("scores_numPlays", 0);
+    mNumRuns        = preferences.getInt("scores_numRuns" , 0);
+    mDeviceID       = preferences.getInt("scores_deviceid",-1);
+    mNumWins        = preferences.getInt("scores_review"  , 0);
+
+    if( mDeviceID==-1 ) mDeviceID = privateGetDeviceID();
+
+    if( thereWasError ) recordDBError(errorStr);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void recordDBError(String message)
+    {
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("scores", message);
+      }
+    else
+      {
+      Exception ex = new Exception(message);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("scores" , message);
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized boolean setRecord(int object, int size, int level, long timeTaken)
+    {
+    int maxsize = ObjectList.getObject(object).getSizes().length;
+
+    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=1 && level<=MAX_LEVEL )
+      {
+      if( mRecords[object][size][level-1]> timeTaken )
+        {
+        mRecords  [object][size][level-1] = timeTaken;
+        mSubmitted[object][size][level-1] = 0;
+        return true;
+        }
+      }
+
+    return false;
+    }
+  }
diff --git a/src/main/java/org/distorted/objects/TwistyDino4.java b/src/main/java/org/distorted/objects/TwistyDino4.java
index 13c33d2d..38e3975d 100644
--- a/src/main/java/org/distorted/objects/TwistyDino4.java
+++ b/src/main/java/org/distorted/objects/TwistyDino4.java
@@ -106,6 +106,23 @@ public class TwistyDino4 extends TwistyDino
 
   public boolean isSolved()
     {
+android.util.Log.e("D",
+
+CUBITS[ 0].mQuatIndex+" "+
+CUBITS[ 1].mQuatIndex+" "+
+CUBITS[ 2].mQuatIndex+" "+
+CUBITS[ 3].mQuatIndex+" "+
+CUBITS[ 4].mQuatIndex+" "+
+CUBITS[ 5].mQuatIndex+" "+
+CUBITS[ 6].mQuatIndex+" "+
+CUBITS[ 7].mQuatIndex+" "+
+CUBITS[ 8].mQuatIndex+" "+
+CUBITS[ 9].mQuatIndex+" "+
+CUBITS[10].mQuatIndex+" "+
+CUBITS[11].mQuatIndex
+
+);
+
     int redX = CUBITS[0].mQuatIndex;
     int bluX = CUBITS[2].mQuatIndex;
     int yelX = CUBITS[8].mQuatIndex;
@@ -114,12 +131,36 @@ public class TwistyDino4 extends TwistyDino
         CUBITS[1].mQuatIndex == bluX && CUBITS[5].mQuatIndex == bluX &&
         CUBITS[9].mQuatIndex == yelX && CUBITS[4].mQuatIndex == yelX  ) return true;
 
-    if (CUBITS[3].mQuatIndex != mulQuat(redX,2)) return false;
-    if (CUBITS[7].mQuatIndex != mulQuat(redX,8)) return false;
-    if (CUBITS[1].mQuatIndex != mulQuat(bluX,2)) return false;
-    if (CUBITS[5].mQuatIndex != mulQuat(bluX,8)) return false;
-    if (CUBITS[9].mQuatIndex != mulQuat(yelX,2)) return false;
-    if (CUBITS[4].mQuatIndex != mulQuat(yelX,8)) return false;
+    if (CUBITS[3].mQuatIndex != mulQuat(redX,2))
+      {
+      android.util.Log.e("D", "FALSE 1");
+      return false;
+      }
+    if (CUBITS[7].mQuatIndex != mulQuat(redX,8))
+      {
+      android.util.Log.e("D", "FALSE 2");
+      return false;
+      }
+    if (CUBITS[1].mQuatIndex != mulQuat(bluX,2))
+      {
+      android.util.Log.e("D", "FALSE 3");
+      return false;
+      }
+    if (CUBITS[5].mQuatIndex != mulQuat(bluX,8))
+      {
+      android.util.Log.e("D", "FALSE 4");
+      return false;
+      }
+    if (CUBITS[9].mQuatIndex != mulQuat(yelX,2))
+      {
+      android.util.Log.e("D", "FALSE 5");
+      return false;
+      }
+    if (CUBITS[4].mQuatIndex != mulQuat(yelX,8))
+      {
+      android.util.Log.e("D", "FALSE 6");
+      return false;
+      }
 
     return true;
     }
diff --git a/src/main/java/org/distorted/scores/RubikScores.java b/src/main/java/org/distorted/scores/RubikScores.java
deleted file mode 100644
index 38887990..00000000
--- a/src/main/java/org/distorted/scores/RubikScores.java
+++ /dev/null
@@ -1,500 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// 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.Context;
-import android.content.SharedPreferences;
-import android.telephony.TelephonyManager;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.main.BuildConfig;
-import org.distorted.objects.ObjectList;
-
-import java.util.UUID;
-
-import static org.distorted.objects.ObjectList.MAX_NUM_OBJECTS;
-import static org.distorted.objects.ObjectList.NUM_OBJECTS;
-import static org.distorted.objects.ObjectList.MAX_LEVEL;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// hold my own scores, and some other statistics.
-
-public class RubikScores
-  {
-  public static final long NO_RECORD = Long.MAX_VALUE;
-  private static RubikScores mThis;
-
-  private final long[][][] mRecords;
-  private final int [][][] mSubmitted;
-
-  private String mName, mCountry;
-  private boolean mNameIsVerified;
-  private int mNumRuns;
-  private int mNumPlays;
-  private int mNumWins;
-  private int mDeviceID;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private RubikScores()
-    {
-    mRecords   = new long[NUM_OBJECTS][MAX_NUM_OBJECTS][MAX_LEVEL];
-    mSubmitted = new int [NUM_OBJECTS][MAX_NUM_OBJECTS][MAX_LEVEL];
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      for(int j=0; j<MAX_NUM_OBJECTS; j++)
-        for(int k=0; k<MAX_LEVEL; k++)
-          {
-          mRecords[i][j][k]   = NO_RECORD;
-          mSubmitted[i][j][k] = 0;
-          }
-
-    mName = "";
-    mCountry = "un";
-
-    mNameIsVerified = false;
-
-    mNumPlays= -1;
-    mNumRuns = -1;
-    mDeviceID= -1;
-    mNumWins =  0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int privateGetDeviceID()
-    {
-    int id;
-
-    try
-      {
-      String s = UUID.randomUUID().toString();
-      id = s.hashCode();
-      }
-    catch(Exception ex)
-      {
-      id = 0;
-      android.util.Log.e("scores", "Exception in getDeviceID()");
-      }
-
-    return id<0 ? -id : id;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized void successfulSubmit()
-    {
-    mNameIsVerified = true;
-
-    for(int i=0; i<NUM_OBJECTS; i++)
-      for(int j=0; j<MAX_NUM_OBJECTS; j++)
-        for(int k=0; k<MAX_LEVEL; k++)
-          {
-          mSubmitted[i][j][k]=1;
-          }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getDeviceID()
-    {
-    return mDeviceID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized boolean thereAreUnsubmittedRecords()
-    {
-    ObjectList list;
-    int length;
-
-    for(int object=0; object<NUM_OBJECTS; object++)
-      {
-      list = ObjectList.getObject(object);
-      length = list.getSizes().length;
-
-      for(int size=0; size<length; size++)
-        for(int level=0; level<MAX_LEVEL; level++)
-          {
-          if( mSubmitted[object][size][level]==0 && mRecords[object][size][level]<NO_RECORD )
-            {
-            return true;
-            }
-          }
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized String getRecordList(String strObj, String strLvl, String strTim)
-    {
-    ObjectList list;
-    StringBuilder builderObj = new StringBuilder();
-    StringBuilder builderLvl = new StringBuilder();
-    StringBuilder builderTim = new StringBuilder();
-    boolean first = true;
-    int[] sizes;
-    int length;
-
-    for(int object=0; object<NUM_OBJECTS; object++)
-      {
-      list = ObjectList.getObject(object);
-      sizes = list.getSizes();
-      length = sizes.length;
-
-      for(int size=0; size<length; size++)
-        {
-        for(int level=0; level<MAX_LEVEL; level++)
-          {
-          if( mSubmitted[object][size][level]==0 && mRecords[object][size][level]<NO_RECORD )
-            {
-            if( !first )
-              {
-              builderObj.append(',');
-              builderLvl.append(',');
-              builderTim.append(',');
-              }
-            else
-              {
-              first=false;
-              }
-
-            builderObj.append(list.name());
-            builderObj.append("_");
-            builderObj.append(sizes[size]);
-            builderLvl.append(level);
-            builderTim.append(mRecords[object][size][level]);
-            }
-          }
-        }
-      }
-
-    return strObj+builderObj.toString()+strLvl+builderLvl.toString()+strTim+builderTim.toString();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Public API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized long getRecord(int object, int size, int level)
-    {
-    int maxsize = ObjectList.getObject(object).getSizes().length;
-
-    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
-      {
-      return mRecords[object][size][level];
-      }
-
-    return -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized boolean isSubmitted(int object, int size, int level)
-    {
-    int maxsize = ObjectList.getObject(object).getSizes().length;
-
-    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
-      {
-      return mSubmitted[object][size][level]==1;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public boolean isVerified()
-    {
-    return mNameIsVerified;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumPlays()
-    {
-    return mNumPlays;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getNumRuns()
-    {
-    return mNumRuns;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getName()
-    {
-    return mName;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getCountry()
-    {
-    return mCountry;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void incrementNumPlays()
-    {
-    mNumPlays++;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void incrementNumRuns()
-    {
-    mNumRuns++;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int incrementNumWins()
-    {
-    mNumWins++;
-    return mNumWins;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setName(String newName)
-    {
-    mName = newName;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized boolean isSolved(int object, int size, int level)
-    {
-    int maxsize = ObjectList.getObject(object).getSizes().length;
-
-    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=0 && level<MAX_LEVEL )
-      {
-      return mRecords[object][size][level]<NO_RECORD;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setCountry(Context context)
-    {
-    TelephonyManager tM =((TelephonyManager) context.getSystemService(Context.TELEPHONY_SERVICE));
-
-    if( tM!=null )
-      {
-      mCountry = tM.getSimCountryIso();
-
-      if( mCountry==null || mCountry.length()<=1 )
-        {
-        mCountry = tM.getNetworkCountryIso();
-        }
-      }
-
-    // Special case: Dominicana. Its ISO-3166-alpha-2 country code is 'do' which we can't have here
-    // because we later on map this to a resource name (the flag) and 'do' is a reserved Java keyword
-    // and can't be a resource name.
-
-    if( mCountry.equals("do") ) mCountry = "dm";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setCountry(String country)
-    {
-    mCountry = country;
-
-    if( mCountry.equals("do") ) mCountry = "dm";  // see above
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static RubikScores getInstance()
-    {
-    if( mThis==null )
-      {
-      mThis = new RubikScores();
-      }
-
-    return mThis;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void savePreferences(SharedPreferences.Editor editor)
-    {
-    StringBuilder builder = new StringBuilder();
-    ObjectList list;
-    int[] sizes;
-    int length;
-
-    for(int level=0; level<MAX_LEVEL; level++)
-      {
-      builder.setLength(0);
-
-      for(int object=0; object<NUM_OBJECTS; object++)
-        {
-        list = ObjectList.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][level]);
-          builder.append(",");
-          builder.append(mSubmitted[object][size][level]);
-          builder.append(" ");
-          }
-        }
-
-      editor.putString("scores_record"+level, builder.toString());
-      }
-
-    editor.putString("scores_name"  , mName  );
-    editor.putBoolean("scores_isVerified", mNameIsVerified);
-    editor.putInt("scores_numPlays", mNumPlays);
-    editor.putInt("scores_numRuns" , mNumRuns );
-    editor.putInt("scores_deviceid", mDeviceID);
-    editor.putInt("scores_review"  , mNumWins );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized void restorePreferences(SharedPreferences preferences)
-    {
-    String recordStr, subStr, nameStr, sizeStr, timeStr, submStr, errorStr="";
-    int start, end, equals, underscore, comma;
-    int object, sizeIndex, subm;
-    long time;
-    boolean thereWasError = false;
-
-    for(int level=0; level<MAX_LEVEL; level++)
-      {
-      start = end = 0;
-      recordStr = preferences.getString("scores_record"+level, "");
-
-      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("=");
-        comma = subStr.indexOf(",");
-
-        if( underscore>=0 && equals>=0 && comma>=0 )
-          {
-          nameStr = subStr.substring(0,underscore);
-          sizeStr = subStr.substring(underscore+1, equals);
-          timeStr = subStr.substring(equals+1,comma);
-          submStr = subStr.substring(comma+1);
-
-          object = ObjectList.getOrdinal(nameStr);
-
-          if( object>=0 && object< NUM_OBJECTS )
-            {
-            sizeIndex = ObjectList.getSizeIndex(object,Integer.parseInt(sizeStr));
-            time = Long.parseLong(timeStr);
-            subm = Integer.parseInt(submStr);
-
-            if( sizeIndex>=0 && sizeIndex<MAX_NUM_OBJECTS && subm>=0 && subm<=1 )
-              {
-              mRecords  [object][sizeIndex][level] = time;
-              mSubmitted[object][sizeIndex][level] = subm;
-              }
-            else
-              {
-              errorStr += ("error1: size="+sizeIndex+" subm="+subm+" obj: "+nameStr+" size: "+sizeStr+"\n");
-              thereWasError= true;
-              }
-            }
-          else
-            {
-            errorStr += ("error2: object="+object+" obj: "+nameStr+" size: "+sizeStr+"\n");
-            thereWasError = true;
-            }
-          }
-        }
-      }
-
-    mName           = preferences.getString("scores_name"  , "" );
-    mNameIsVerified = preferences.getBoolean("scores_isVerified", false);
-    mNumPlays       = preferences.getInt("scores_numPlays", 0);
-    mNumRuns        = preferences.getInt("scores_numRuns" , 0);
-    mDeviceID       = preferences.getInt("scores_deviceid",-1);
-    mNumWins        = preferences.getInt("scores_review"  , 0);
-
-    if( mDeviceID==-1 ) mDeviceID = privateGetDeviceID();
-
-    if( thereWasError ) recordDBError(errorStr);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void recordDBError(String message)
-    {
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.e("scores", message);
-      }
-    else
-      {
-      Exception ex = new Exception(message);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("scores" , message);
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized boolean setRecord(int object, int size, int level, long timeTaken)
-    {
-    int maxsize = ObjectList.getObject(object).getSizes().length;
-
-    if( object>=0 && object<NUM_OBJECTS && size>=0 && size<maxsize && level>=1 && level<=MAX_LEVEL )
-      {
-      if( mRecords[object][size][level-1]> timeTaken )
-        {
-        mRecords  [object][size][level-1] = timeTaken;
-        mSubmitted[object][size][level-1] = 0;
-        return true;
-        }
-      }
-
-    return false;
-    }
-  }
diff --git a/src/main/java/org/distorted/scores/RubikScoresDownloader.java b/src/main/java/org/distorted/scores/RubikScoresDownloader.java
deleted file mode 100644
index 25133a0f..00000000
--- a/src/main/java/org/distorted/scores/RubikScoresDownloader.java
+++ /dev/null
@@ -1,499 +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.scores;
-
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-
-import androidx.fragment.app.FragmentActivity;
-
-import org.distorted.objects.ObjectList;
-
-import java.io.InputStream;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.UnknownHostException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-import static org.distorted.objects.ObjectList.MAX_LEVEL;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikScoresDownloader implements Runnable
-  {
-  public interface Receiver
-    {
-    void receive(String[][][] country, String[][][] name, float[][][] time);
-    void message(String mess);
-    void error(String error);
-    }
-
-  public static final int MAX_PLACES = 10;
-
-  private static final int DOWNLOAD   = 0;
-  private static final int SUBMIT     = 1;
-  private static final int IDLE       = 2;
-
-  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 final int mTotal = ObjectList.getTotal();
-  private static final String[][][] mCountry = new String[mTotal][MAX_LEVEL][MAX_PLACES];
-  private static final String[][][] mName    = new String[mTotal][MAX_LEVEL][MAX_PLACES];
-  private static final  float[][][] mTime    = new  float[mTotal][MAX_LEVEL][MAX_PLACES];
-  private static final int[][] mPlaces = new int[mTotal][MAX_LEVEL];
-
-  private static RubikScoresDownloader mThis;
-  private static String mScores = "";
-  private static boolean mRunning = false;
-  private static int mMode = IDLE;
-  private static Receiver mReceiver;
-  private static String mVersion;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static String computeHash(String stringToHash, byte[] salt)
-    {
-    String generatedPassword;
-
-    try
-      {
-      MessageDigest md = MessageDigest.getInstance("MD5");
-      md.update(salt);
-      byte[] bytes = md.digest(stringToHash.getBytes());
-      StringBuilder sb = new StringBuilder();
-
-      for (byte aByte : bytes)
-        {
-        sb.append(Integer.toString((aByte & 0xff) + 0x100, 16).substring(1));
-        }
-
-      generatedPassword = sb.toString();
-      }
-    catch (NoSuchAlgorithmException e)
-      {
-      return "NoSuchAlgorithm";
-      }
-
-    return generatedPassword;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean fillValues()
-    {
-    int begin=-1 ,end, len = mScores.length();
-    String row;
-
-    if( len==0 )
-      {
-      mReceiver.error("1");
-      return false;
-      }
-    else if( len<=2 )
-      {
-      mReceiver.error(mScores);
-      return false;
-      }
-
-    for(int i=0; i<mTotal; i++)
-      for(int j=0; j<MAX_LEVEL; j++)
-        {
-        mPlaces[i][j] = 0;
-        }
-
-    while( begin<len )
-      {
-      end = mScores.indexOf('\n', begin+1);
-      if( end<0 ) end = len;
-
-      try
-        {
-        row = mScores.substring(begin+1,end);
-        fillRow(row);
-        }
-      catch(Exception ex)
-        {
-        // faulty row - ignore
-        }
-
-      begin = end;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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.length();
-
-    if( s5>s4 && s4>s3 && s3>s2 && s2>s1 && s1>0 )
-      {
-      int object = ObjectList.unpackObjectFromString( row.substring(0,s1) );
-
-      if( object>=0 && object<mTotal )
-        {
-        int level      = Integer.parseInt( row.substring(s1+1,s2) );
-        String name    = row.substring(s2+1, s3);
-        int time       = Integer.parseInt( row.substring(s3+1,s4) );
-        String country = row.substring(s4+1, s5);
-
-        if( country.equals("do") ) country = "dm"; // see RubikScores.setCountry()
-
-        if(level>=0 && level<MAX_LEVEL)
-          {
-          int p = mPlaces[object][level];
-          mPlaces[object][level]++;
-
-          mCountry[object][level][p] = country;
-          mName   [object][level][p] = name;
-          mTime   [object][level][p] = ((float)(time/10))/100.0f;
-          }
-        }
-      }
-    else
-      {
-      tryDoCommand(row);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void tryDoCommand(String row)
-    {
-    if( row.startsWith("comm") )
-      {
-      int colon = row.indexOf(':');
-
-      if( colon>0 )
-        {
-        String commandNumber = row.substring(4,colon);
-        int number;
-
-        try
-          {
-          number = Integer.parseInt(commandNumber);
-          }
-        catch(NumberFormatException ex)
-          {
-          number=0;
-          }
-
-        if(number==1)
-          {
-          String country = row.substring(colon+1);
-          RubikScores scores = RubikScores.getInstance();
-          scores.setCountry(country);
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 network(String url)
-    {
-    try
-      {
-      java.net.URL connectURL = new URL(url);
-      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.message("Failed to get an answer from the High Scores server");
-        return false;
-        }
-      }
-    catch( final UnknownHostException e )
-      {
-      mReceiver.message("No access to Internet");
-      return false;
-      }
-    catch( final SecurityException e )
-      {
-      mReceiver.message("Application not authorized to connect to the Internet");
-      return false;
-      }
-    catch( final Exception e )
-      {
-      mReceiver.message(e.getMessage());
-      return false;
-      }
-
-    if( mScores.length()==0 )
-      {
-      mReceiver.message("Failed to download scores");
-      return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String constructDownloadURL()
-    {
-    RubikScores scores = RubikScores.getInstance();
-    String name = URLencode(scores.getName());
-    String veri = scores.isVerified() ? name : "";
-    int numRuns = scores.getNumRuns();
-    int numPlay = scores.getNumPlays();
-    String country = scores.getCountry();
-
-    String url="https://distorted.org/magic/cgi-bin/download.cgi";
-    url += "?n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion+"d";
-    url += "&o="+ ObjectList.getObjectList()+"&min=0&max="+MAX_LEVEL+"&l="+MAX_PLACES;
-
-    return url;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String constructSubmitURL()
-    {
-    RubikScores scores = RubikScores.getInstance();
-    String name = URLencode(scores.getName());
-    String veri = scores.isVerified() ? name : "";
-    int numRuns = scores.getNumRuns();
-    int numPlay = scores.getNumPlays();
-    int deviceID= scores.getDeviceID();
-    String reclist = scores.getRecordList("&o=","&l=","&t=");
-    String country = scores.getCountry();
-    long epoch = System.currentTimeMillis();
-    String salt = "cuboid";
-
-    String url1="https://distorted.org/magic/cgi-bin/submit.cgi";
-    String url2 = "n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&i="+deviceID+"&e="+mVersion+"d";
-    url2 += reclist+"&c="+country+"&f="+epoch+"&oo="+ ObjectList.getObjectList();
-    url2 += "&min=0&max="+MAX_LEVEL+"&lo="+MAX_PLACES;
-    String hash = computeHash( url2, salt.getBytes() );
-
-    return url1 + "?" + url2 + "&h=" + hash;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean gottaDownload()
-    {
-    return ((mScores.length()==0) && !mRunning);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  @Override
-  public void run()
-    {
-    boolean success=true;
-
-    try
-      {
-      if( mMode==DOWNLOAD && gottaDownload() )
-        {
-        mRunning = true;
-        success = network(constructDownloadURL());
-        }
-      if( mMode==SUBMIT )
-        {
-        mRunning = true;
-
-        if( RubikScores.getInstance().thereAreUnsubmittedRecords() )
-          {
-          success = network(constructSubmitURL());
-          }
-        }
-      }
-    catch( Exception e )
-      {
-      mReceiver.message("Exception downloading records: "+e.getMessage() );
-      }
-
-    if( mRunning )
-      {
-      success = fillValues();
-      mRunning = false;
-      }
-
-    if( success )
-      {
-      mReceiver.receive(mCountry, mName, mTime);
-
-      if( mMode==SUBMIT )
-        {
-        RubikScores.getInstance().successfulSubmit();
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private RubikScoresDownloader()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void onPause()
-    {
-    mRunning = false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static RubikScoresDownloader getInstance()
-    {
-    if( mThis==null )
-      {
-      mThis = new RubikScoresDownloader();
-      }
-
-    return mThis;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void start(Receiver receiver, FragmentActivity act, int mode)
-    {
-    mReceiver = receiver;
-    mMode     = mode;
-
-    try
-      {
-      PackageInfo pInfo = act.getPackageManager().getPackageInfo( act.getPackageName(), 0);
-      mVersion = pInfo.versionName;
-      }
-    catch (PackageManager.NameNotFoundException e)
-      {
-      mVersion = "0.9.2";
-      }
-
-    Thread networkThrd = new Thread(this);
-    networkThrd.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void download(Receiver receiver, FragmentActivity act)
-    {
-    start(receiver, act, DOWNLOAD);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void submit(Receiver receiver, FragmentActivity act)
-    {
-    start(receiver, act, SUBMIT);
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/states/RubikStatePlay.java b/src/main/java/org/distorted/states/RubikStatePlay.java
index 53d66b29..55dbbe61 100644
--- a/src/main/java/org/distorted/states/RubikStatePlay.java
+++ b/src/main/java/org/distorted/states/RubikStatePlay.java
@@ -41,7 +41,7 @@ import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
 import org.distorted.objects.ObjectList;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/states/RubikStateSolving.java b/src/main/java/org/distorted/states/RubikStateSolving.java
index a7373630..495af9ad 100644
--- a/src/main/java/org/distorted/states/RubikStateSolving.java
+++ b/src/main/java/org/distorted/states/RubikStateSolving.java
@@ -30,7 +30,7 @@ import android.widget.TextView;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 
 import java.util.Timer;
 import java.util.TimerTask;
diff --git a/src/main/java/org/distorted/tutorials/TutorialPreRender.java b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
index f8af705d..4d4c6817 100644
--- a/src/main/java/org/distorted/tutorials/TutorialPreRender.java
+++ b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
@@ -27,7 +27,7 @@ import org.distorted.effects.EffectController;
 import org.distorted.objects.ObjectList;
 import org.distorted.objects.TwistyObject;
 import org.distorted.main.RubikPreRender.ActionFinishedListener;
-import org.distorted.scores.RubikScores;
+import org.distorted.network.RubikScores;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/res/raw/diam2.dmesh b/src/main/res/raw/diam2.dmesh
new file mode 100644
index 00000000..2ffc96ec
Binary files /dev/null and b/src/main/res/raw/diam2.dmesh differ
diff --git a/src/main/res/raw/skew2.dmesh b/src/main/res/raw/skew2.dmesh
new file mode 100644
index 00000000..483505f7
Binary files /dev/null and b/src/main/res/raw/skew2.dmesh differ
diff --git a/src/main/res/raw/skew3.dmesh b/src/main/res/raw/skew3.dmesh
new file mode 100644
index 00000000..4b66c20b
Binary files /dev/null and b/src/main/res/raw/skew3.dmesh differ
