commit acabdd838d092a158f2a14d3fee627d641e04ab3
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Fri Jan 28 22:17:12 2022 +0100

    Rename the 'network' package to 'external' since it will also deal with writing/reading files from local storage.

diff --git a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
index 30a8ab80..39ad074a 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogNewRecord.java
@@ -37,10 +37,9 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.network.RubikScores;
+import org.distorted.external.RubikScores;
 import org.distorted.objects.RubikObjectList;
 import org.distorted.screens.ScreenList;
-import org.distorted.screens.RubikScreenPlay;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
index 9a971ed7..364ca832 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresPagerAdapter.java
@@ -31,8 +31,8 @@ import android.view.ViewGroup;
 import android.widget.LinearLayout;
 
 import org.distorted.main.R;
-import org.distorted.network.RubikScores;
-import org.distorted.network.RubikNetwork;
+import org.distorted.external.RubikScores;
+import org.distorted.external.RubikNetwork;
 import org.distorted.objects.RubikObjectList;
 import org.distorted.screens.RubikScreenPlay;
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
index f97f4014..dcd3426c 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogScoresView.java
@@ -33,9 +33,9 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.network.RubikScores;
+import org.distorted.external.RubikScores;
 
-import static org.distorted.network.RubikNetwork.MAX_PLACES;
+import static org.distorted.external.RubikNetwork.MAX_PLACES;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
index 69d8ef90..197ecbc6 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogSetName.java
@@ -40,10 +40,9 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.network.RubikScores;
+import org.distorted.external.RubikScores;
 import org.distorted.objects.RubikObjectList;
 import org.distorted.screens.ScreenList;
-import org.distorted.screens.RubikScreenPlay;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogUpdateView.java b/src/main/java/org/distorted/dialogs/RubikDialogUpdateView.java
index 2294f014..7202a0ad 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogUpdateView.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogUpdateView.java
@@ -31,13 +31,8 @@ import android.widget.ProgressBar;
 import android.widget.TextView;
 
 import org.distorted.main.R;
-import org.distorted.network.RubikNetwork;
-import org.distorted.network.RubikUpdates;
-
-import java.io.InputStream;
-
-import static android.view.View.GONE;
-import static android.view.View.VISIBLE;
+import org.distorted.external.RubikNetwork;
+import org.distorted.external.RubikUpdates;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -80,7 +75,7 @@ public class RubikDialogUpdateView implements RubikNetwork.Downloadee
 
     if( info.mPercent>=100 )
       {
-      mBar.setVisibility(GONE);
+      mBar.setVisibility(View.GONE);
       mButton.setOnClickListener( new View.OnClickListener()
         {
         @Override
@@ -98,7 +93,7 @@ public class RubikDialogUpdateView implements RubikNetwork.Downloadee
       }
     else
       {
-      mButton.setVisibility(GONE);
+      mButton.setVisibility(View.GONE);
       mBar.setLayoutParams(pButt);
       mBar.setProgress(info.mPercent);
       }
@@ -119,8 +114,8 @@ public class RubikDialogUpdateView implements RubikNetwork.Downloadee
     {
     mDescription.setText(R.string.downloading);
     mBar.setProgress(20);
-    mButton.setVisibility(GONE);
-    mBar.setVisibility(VISIBLE);
+    mButton.setVisibility(View.GONE);
+    mBar.setVisibility(View.VISIBLE);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/dialogs/RubikDialogUpdates.java b/src/main/java/org/distorted/dialogs/RubikDialogUpdates.java
index 9d62feda..f4cec98e 100644
--- a/src/main/java/org/distorted/dialogs/RubikDialogUpdates.java
+++ b/src/main/java/org/distorted/dialogs/RubikDialogUpdates.java
@@ -41,8 +41,8 @@ import androidx.fragment.app.FragmentActivity;
 
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.network.RubikNetwork;
-import org.distorted.network.RubikUpdates;
+import org.distorted.external.RubikNetwork;
+import org.distorted.external.RubikUpdates;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/external/RubikNetwork.java b/src/main/java/org/distorted/external/RubikNetwork.java
new file mode 100644
index 00000000..5a097eb9
--- /dev/null
+++ b/src/main/java/org/distorted/external/RubikNetwork.java
@@ -0,0 +1,934 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.external;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.UnknownHostException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.graphics.Bitmap;
+import android.graphics.BitmapFactory;
+
+import androidx.fragment.app.FragmentActivity;
+
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.objectlib.json.JsonWriter;
+import org.distorted.objects.RubikObjectList;
+
+import static org.distorted.objects.RubikObjectList.MAX_LEVEL;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikNetwork
+  {
+  public interface ScoresReceiver
+    {
+    void receive(String[][][] country, String[][][] name, float[][][] time);
+    void message(String mess);
+    void error(String error);
+    }
+
+  public interface IconReceiver
+    {
+    void iconDownloaded(int ordinal, Bitmap bitmap);
+    }
+
+  public interface Updatee
+    {
+    void receiveUpdate(RubikUpdates update);
+    void errorUpdate();
+    }
+
+  public interface Downloadee
+    {
+    void jsonDownloaded();
+    }
+
+  public static final int MAX_PLACES = 10;
+
+  private static final int REND_ADRENO= 0;
+  private static final int REND_MALI  = 1;
+  private static final int REND_POWER = 2;
+  private static final int REND_OTHER = 3;
+
+  private static final int DEBUG_RUNNING = 1;
+  private static final int DEBUG_SUCCESS = 2;
+  private static final int DEBUG_FAILURE = 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 String[][][] mCountry;
+  private static String[][][] mName;
+  private static float[][][] mTime;
+  private static int[][] mPlaces;
+
+  private static RubikNetwork mThis;
+  private static String mScores = "";
+  private static boolean mRunning = false;
+  private static Updatee mUpdatee;
+  private static String mVersion;
+  private static int mNumObjects;
+  private static RubikUpdates mUpdates;
+  private static int mDebugState;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private static void initializeStatics()
+    {
+    int newNum = RubikObjectList.getNumObjects();
+
+    if( mCountry==null || newNum!=mNumObjects ) mCountry = new String[newNum][MAX_LEVEL][MAX_PLACES];
+    if( mName==null    || newNum!=mNumObjects ) mName    = new String[newNum][MAX_LEVEL][MAX_PLACES];
+    if( mTime==null    || newNum!=mNumObjects ) mTime    = new  float[newNum][MAX_LEVEL][MAX_PLACES];
+    if( mPlaces==null  || newNum!=mNumObjects ) mPlaces  = new    int[newNum][MAX_LEVEL];
+
+    if( mUpdates==null ) mUpdates = new RubikUpdates();
+
+    mNumObjects = newNum;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 fillValuesNormal(ScoresReceiver receiver)
+    {
+    int begin=-1 ,end, len = mScores.length();
+    String row;
+
+    if( len==0 )
+      {
+      receiver.error("1");
+      return false;
+      }
+    else if( len<=2 )
+      {
+      receiver.error(mScores);
+      return false;
+      }
+
+    for(int i=0; i<mNumObjects; 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 = RubikObjectList.getOrdinal( row.substring(0,s1) );
+
+      if( object>=0 && object<mNumObjects )
+        {
+        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 int getRendererType(String renderer)
+    {
+    if( renderer.contains("Adreno")  ) return REND_ADRENO;
+    if( renderer.contains("Mali")    ) return REND_MALI;
+    if( renderer.contains("PowerVR") ) return REND_POWER;
+
+    return REND_OTHER;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String parseRenderer(final int type, String renderer)
+    {
+    if( type==REND_ADRENO || type==REND_POWER )
+      {
+      int lastSpace = renderer.lastIndexOf(' ');
+      String ret = renderer.substring(lastSpace+1);
+      return URLencode(ret);
+      }
+
+    if( type==REND_MALI )
+      {
+      int firstHyphen = renderer.indexOf('-');
+      String ret = renderer.substring(firstHyphen+1);
+      return URLencode(ret);
+      }
+
+    return "other";
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String parseVersion(final int type, String version)
+    {
+    switch(type)
+      {
+      case REND_ADRENO: int aMonkey = version.indexOf('@');
+                        int aDot = version.indexOf('.', aMonkey);
+                        String ret1 = aDot>=3 ? version.substring(aDot-3,aDot) : "";
+                        return URLencode(ret1);
+      case REND_MALI  : int mV1 = version.indexOf("v1");
+                        int mHyphen = version.indexOf('-', mV1);
+                        String ret2 = mHyphen>mV1+3 && mV1>=0 ? version.substring(mV1+3,mHyphen) : "";
+                        return URLencode(ret2);
+      case REND_POWER : int pMonkey = version.indexOf('@');
+                        int pSpace  = version.lastIndexOf(' ');
+                        String ret3 = pSpace>=0 && pMonkey>pSpace+1 ? version.substring(pSpace+1,pMonkey) : "";
+                        return URLencode(ret3);
+      default         : return "";
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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, ScoresReceiver receiver)
+    {
+    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();
+
+      InputStream is = conn.getInputStream();
+      BufferedReader r = new BufferedReader(new InputStreamReader(is));
+      StringBuilder total = new StringBuilder();
+
+      for (String line; (line = r.readLine()) != null; )
+        {
+        total.append(line).append('\n');
+        }
+
+      mScores = total.toString();
+      conn.disconnect();
+      }
+    catch( final UnknownHostException e )
+      {
+      receiver.message("No access to Internet");
+      return false;
+      }
+    catch( final SecurityException e )
+      {
+      receiver.message("Application not authorized to connect to the Internet");
+      return false;
+      }
+    catch( final Exception e )
+      {
+      receiver.message(e.getMessage());
+      return false;
+      }
+
+    if( mScores.length()==0 )
+      {
+      receiver.message("Failed to download scores");
+      return false;
+      }
+
+    return true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String constructSuspiciousURL(String suspURL)
+    {
+    RubikScores scores = RubikScores.getInstance();
+    int deviceID= scores.getDeviceID();
+    String suspicious = URLencode(suspURL);
+
+    String url="https://distorted.org/magic/cgi-bin/suspicious.cgi";
+    url += "?i="+deviceID+"&d="+suspicious;
+
+    return url;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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();
+    int objectAPI   = JsonWriter.VERSION_OBJECT_MAJOR;
+    int tutorialAPI = JsonWriter.VERSION_EXTRAS_MAJOR;
+
+    renderer = URLencode(renderer);
+    version  = URLencode(version);
+
+    String url="https://distorted.org/magic/cgi-bin/debugs-new.cgi";
+    url += "?n="+name+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion+"d";
+    url += "&d="+renderer+"&v="+version+"&a="+objectAPI+"&b="+tutorialAPI;
+
+    return url;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String constructDownloadURL()
+    {
+    RubikScores scores = RubikScores.getInstance();
+    String name = URLencode(scores.getName());
+    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+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion;
+
+    return url;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String constructSubmitURL()
+    {
+    RubikScores scores = RubikScores.getInstance();
+    String name = URLencode(scores.getName());
+    String veri = scores.isVerified() ? "1" : "";
+    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 renderer = DistortedLibrary.getDriverRenderer();
+    String version  = DistortedLibrary.getDriverVersion();
+
+    int type = getRendererType(renderer);
+    renderer = parseRenderer(type,renderer);
+    version  = parseVersion(type,version);
+
+    String url1="https://distorted.org/magic/cgi-bin/submit.cgi";
+    String url2 = "n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&i="+deviceID+"&e="+mVersion;
+    url2 += "&d="+renderer+"&s="+version+reclist+"&c="+country+"&f="+epoch;
+    String hash = computeHash( url2, salt.getBytes() );
+
+    return url1 + "?" + url2 + "&h=" + hash;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean gottaDownload()
+    {
+    return ((mScores.length()==0) && !mRunning);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void figureOutVersion(FragmentActivity act)
+    {
+    try
+      {
+      PackageInfo pInfo = act.getPackageManager().getPackageInfo( act.getPackageName(), 0);
+      mVersion = pInfo.versionName;
+      }
+    catch (PackageManager.NameNotFoundException e)
+      {
+      mVersion = "0.9.2";
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void downloadThread(ScoresReceiver receiver)
+    {
+    try
+      {
+      if( gottaDownload() )
+        {
+        mRunning = true;
+        boolean receiveValues = network(constructDownloadURL(),receiver);
+
+        if( mRunning )
+          {
+          receiveValues = fillValuesNormal(receiver);
+          mRunning = false;
+          }
+
+        if( receiveValues ) receiver.receive(mCountry, mName, mTime);
+        }
+      }
+    catch( Exception e )
+      {
+      receiver.message("Exception downloading records: "+e.getMessage() );
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void submitThread(ScoresReceiver receiver)
+    {
+    try
+      {
+      mRunning = true;
+      RubikScores scores = RubikScores.getInstance();
+
+      if( scores.thereAreUnsubmittedRecords() )
+        {
+        boolean receiveValues = network(constructSubmitURL(),receiver);
+
+        if( mRunning )
+          {
+          receiveValues = fillValuesNormal(receiver);
+          mRunning = false;
+          }
+
+        if( receiveValues )
+          {
+          RubikScores.getInstance().successfulSubmit();
+          receiver.receive(mCountry, mName, mTime);
+          }
+        }
+      }
+    catch( Exception e )
+      {
+      receiver.message("Exception submitting records: "+e.getMessage() );
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void debugThread()
+    {
+    String url = constructDebugURL();
+/*
+    try { Thread.sleep(5000); }
+    catch( InterruptedException ignored) {}
+*/
+    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();
+
+      InputStream is = conn.getInputStream();
+      BufferedReader r = new BufferedReader(new InputStreamReader(is));
+      StringBuilder answer = new StringBuilder();
+
+      for (String line; (line = r.readLine()) != null; )
+        {
+        answer.append(line).append('\n');
+        }
+
+      String updates = answer.toString();
+      conn.disconnect();
+      mUpdates.parse(updates);
+
+      if( mUpdatee!=null ) mUpdatee.receiveUpdate(mUpdates);
+      mDebugState = DEBUG_SUCCESS;
+      }
+    catch( final Exception e )
+      {
+      if( mUpdatee!=null ) mUpdatee.errorUpdate();
+      mDebugState = DEBUG_FAILURE;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void suspiciousThread(String suspURL)
+    {
+    String url = constructSuspiciousURL(suspURL);
+
+    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();
+      conn.disconnect();
+      }
+    catch( final Exception e )
+      {
+      // ignore
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Bitmap downloadIcon(String url)
+    {
+    try
+      {
+      java.net.URL connectURL = new URL(url);
+      HttpURLConnection conn = (HttpURLConnection) connectURL.openConnection();
+      conn.setDoInput(true);
+      conn.connect();
+      InputStream input = conn.getInputStream();
+      Bitmap icon = BitmapFactory.decodeStream(input);
+      conn.disconnect();
+      return icon;
+      }
+    catch (IOException e)
+      {
+      android.util.Log.e("D", "Failed to download "+url);
+      android.util.Log.e("D", e.getMessage() );
+      return null;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void iconThread(IconReceiver receiver)
+    {
+    int numC = mUpdates.getCompletedNumber();
+    int numS = mUpdates.getStartedNumber();
+
+    for(int c=0; c<numC; c++)
+      {
+      int iconPresent = mUpdates.getCompletedIconPresent(c);
+
+      if( iconPresent!=0 )
+        {
+        Bitmap icon = mUpdates.getCompletedIcon(c);
+
+        if( icon==null )
+          {
+          String url = mUpdates.getCompletedURL(c);
+          icon = downloadIcon(url);
+          }
+        if( icon!=null )
+          {
+          mUpdates.setCompletedIcon(c,icon);
+          receiver.iconDownloaded(c,icon);
+          }
+        }
+      }
+
+    for(int s=0; s<numS; s++)
+      {
+      int iconPresent = mUpdates.getStartedIconPresent(s);
+
+      if( iconPresent!=0 )
+        {
+        Bitmap icon = mUpdates.getStartedIcon(s);
+
+        if( icon==null )
+          {
+          String url = mUpdates.getStartedURL(s);
+          icon = downloadIcon(url);
+          }
+        if( icon!=null )
+          {
+          mUpdates.setStartedIcon(s,icon);
+          receiver.iconDownloaded(numC+s,icon);
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private InputStream downloadJSON(String name)
+    {
+    String url = mUpdates.getURL() + name;
+
+    try
+      {
+      java.net.URL connectURL = new URL(url);
+      HttpURLConnection conn = (HttpURLConnection) connectURL.openConnection();
+      conn.setDoInput(true);
+      conn.connect();
+      InputStream stream = conn.getInputStream();
+      conn.disconnect();
+      return stream;
+      }
+    catch (IOException e)
+      {
+      android.util.Log.e("D", "Failed to download "+url);
+      android.util.Log.e("D", e.getMessage() );
+      return null;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void jsonThread(final RubikUpdates.UpdateInfo info, Downloadee downloadee)
+    {
+    if(info.mUpdateObject) info.mObjectStream = downloadJSON(info.mObjectShortName+"_object.json");
+    if(info.mUpdateExtras) info.mExtrasStream = downloadJSON(info.mObjectShortName+"_extras.json");
+
+    downloadee.jsonDownloaded();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RubikNetwork()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void onPause()
+    {
+    mRunning = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static RubikNetwork getInstance()
+    {
+    if( mThis==null )
+      {
+      mThis = new RubikNetwork();
+      }
+
+    return mThis;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void download(final ScoresReceiver receiver, final FragmentActivity act)
+    {
+    initializeStatics();
+    figureOutVersion(act);
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        downloadThread(receiver);
+        }
+      };
+
+    thread.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void submit(ScoresReceiver receiver, final FragmentActivity act)
+    {
+    initializeStatics();
+    figureOutVersion(act);
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        submitThread(receiver);
+        }
+      };
+
+    thread.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void debug()
+    {
+    initializeStatics();
+    mDebugState = DEBUG_RUNNING;
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        debugThread();
+        }
+      };
+
+    thread.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void suspicious(final String suspicious)
+    {
+    initializeStatics();
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        suspiciousThread(suspicious);
+        }
+      };
+
+    thread.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Yes it can happen that the second Updatee registers before we sent an update to the first one
+// and, as a result, the update never gets sent to the first one. This is not a problem (now, when
+// there are only two updatees - the RubikStatePlay and the UpdateDialog)
+//
+// Yes, there is also a remote possibility that the two threads executing this function and executing
+// the sendDebug() get swapped exactly in unlucky moment and the update never gets to the updatee.
+// We don't care about such remote possibility, then the app simply would signal that there are no
+// updates available.
+
+  public void signUpForUpdates(Updatee updatee)
+    {
+         if( mDebugState==DEBUG_SUCCESS ) updatee.receiveUpdate(mUpdates);
+    else if( mDebugState==DEBUG_FAILURE ) updatee.errorUpdate();
+    else mUpdatee = updatee;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void downloadIcons(final IconReceiver receiver)
+    {
+    initializeStatics();
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        iconThread(receiver);
+        }
+      };
+
+    thread.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void downloadJSON(final RubikUpdates.UpdateInfo info, final Downloadee downloadee)
+    {
+    initializeStatics();
+
+    Thread thread = new Thread()
+      {
+      public void run()
+        {
+        jsonThread(info,downloadee);
+        }
+      };
+
+    thread.start();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void updateDone(String shortName)
+    {
+    mUpdates.updateDone(shortName);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/external/RubikScores.java b/src/main/java/org/distorted/external/RubikScores.java
new file mode 100644
index 00000000..ae792841
--- /dev/null
+++ b/src/main/java/org/distorted/external/RubikScores.java
@@ -0,0 +1,458 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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.external;
+
+import java.util.HashMap;
+import java.util.UUID;
+
+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.RubikObject;
+import org.distorted.objects.RubikObjectList;
+
+import static org.distorted.objects.RubikObjectList.MAX_LEVEL;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// hold my own scores, and some other statistics.
+
+public class RubikScores
+  {
+  public static final int MULT = 1000000;
+  public static final long NO_RECORD = Long.MAX_VALUE;
+  private static RubikScores mThis;
+
+  private String mName, mCountry;
+  private boolean mNameIsVerified;
+  private int mNumRuns;
+  private int mNumPlays;
+  private int mNumWins;
+  private int mDeviceID;
+
+  private static class MapValue
+    {
+    long record;
+    boolean submitted;
+
+    MapValue(long rec,int sub)
+      {
+      record    = rec;
+      submitted = sub!=0;
+      }
+    }
+
+  private final HashMap<Integer,MapValue> mMap;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private RubikScores()
+    {
+    mMap = new HashMap<>();
+
+    mName = "";
+    mCountry = "un";
+
+    mNameIsVerified = false;
+
+    mNumPlays= -1;
+    mNumRuns = -1;
+    mDeviceID= -1;
+    mNumWins =  0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int mapKey(int object,int level)
+    {
+    return object*MULT + level;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 key: mMap.keySet())
+      {
+      MapValue value = mMap.get(key);
+      if( value!=null ) value.submitted = true;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getDeviceID()
+    {
+    return mDeviceID;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized boolean thereAreUnsubmittedRecords()
+    {
+    for(int key: mMap.keySet())
+      {
+      MapValue value = mMap.get(key);
+      if( value!=null && !value.submitted && value.record<NO_RECORD) return true;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  synchronized String getRecordList(String strObj, String strLvl, String strTim)
+    {
+    StringBuilder builderObj = new StringBuilder();
+    StringBuilder builderLvl = new StringBuilder();
+    StringBuilder builderTim = new StringBuilder();
+    boolean first = true;
+
+    for(int key: mMap.keySet())
+      {
+      MapValue value = mMap.get(key);
+
+      if( value!=null && !value.submitted && value.record<NO_RECORD)
+        {
+        if( !first )
+          {
+          builderObj.append(',');
+          builderLvl.append(',');
+          builderTim.append(',');
+          }
+        first=false;
+
+        RubikObject object = RubikObjectList.getObject(key/MULT);
+
+        if( object!=null )
+          {
+          builderObj.append(object.getName());
+          builderLvl.append(key%MULT);
+          builderTim.append(value.record);
+          }
+        }
+      }
+
+    return strObj+builderObj.toString()+strLvl+builderLvl.toString()+strTim+builderTim.toString();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Public API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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 setRecord(int object, int level, long record)
+    {
+    int key = mapKey(object,level)-1; // -1 - historical reasons; previous versions saved it like this.
+    MapValue oldValue = mMap.get(key);
+
+    if( oldValue==null )
+      {
+      MapValue value = new MapValue(record,0);
+      mMap.put(key,value);
+      return true;
+      }
+
+    long oldRecord = oldValue.record;
+
+    if( oldRecord>record)
+      {
+      MapValue value = new MapValue(record,0);
+      mMap.put(key,value);
+      return true;
+      }
+
+    return false;
+    }
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized long getRecord(int object, int level)
+    {
+    int key = mapKey(object,level);
+    MapValue value = mMap.get(key);
+    return value!=null ? value.record : NO_RECORD;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public synchronized boolean isSolved(int object, int level)
+    {
+    int key = mapKey(object,level);
+    MapValue value = mMap.get(key);
+    return value!=null && value.record<NO_RECORD;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  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)
+    {
+    int numObjects = RubikObjectList.getNumObjects();
+    StringBuilder builder = new StringBuilder();
+
+    for(int level=0; level<MAX_LEVEL; level++)
+      {
+      builder.setLength(0);
+
+      for(int object=0; object<numObjects; object++)
+        {
+        int key = mapKey(object,level);
+        RubikObject obj = RubikObjectList.getObject(object);
+        MapValue value = mMap.get(key);
+
+        if( obj!=null && value!=null && value.record<NO_RECORD )
+          {
+          builder.append(obj.getName());
+          builder.append("=");
+          builder.append(value.record);
+          builder.append(",");
+          builder.append(value.submitted ? 1:0 );
+          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, timeStr, submStr, errorStr="";
+    int start, end, equals, comma, object, subm;
+    long time;
+    boolean thereWasError = false;
+    int numObjects = RubikObjectList.getNumObjects();
+
+    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;
+
+        equals = subStr.indexOf("=");
+        comma  = subStr.indexOf(",");
+
+        if( equals>=0 && comma>=0 )
+          {
+          nameStr = subStr.substring(0,equals);
+          timeStr = subStr.substring(equals+1,comma);
+          submStr = subStr.substring(comma+1);
+
+          object = RubikObjectList.getOrdinal(nameStr);
+
+          if( object>=0 && object<numObjects )
+            {
+            time = Long.parseLong(timeStr);
+            subm = Integer.parseInt(submStr);
+
+            if( subm>=0 && subm<=1 )
+              {
+              MapValue value = new MapValue(time,subm);
+              int key = mapKey(object,level);
+              mMap.put(key,value);
+              }
+            else
+              {
+              errorStr += ("error1: subm="+subm+" obj: "+nameStr+"\n");
+              thereWasError= true;
+              }
+            }
+          else
+            {
+            errorStr += ("error2: object="+object+" obj: "+nameStr+"\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);
+      }
+    }
+  }
diff --git a/src/main/java/org/distorted/external/RubikUpdates.java b/src/main/java/org/distorted/external/RubikUpdates.java
new file mode 100644
index 00000000..d65c8316
--- /dev/null
+++ b/src/main/java/org/distorted/external/RubikUpdates.java
@@ -0,0 +1,274 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2022 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.external;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import android.graphics.Bitmap;
+import org.distorted.objects.RubikObjectList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class RubikUpdates
+{
+  public static class UpdateInfo
+    {
+    public final String mObjectShortName;
+    public final String mObjectLongName;
+    public final String mDescription;
+    public final int mObjectMinorVersion;
+    public final int mExtrasMinorVersion;
+    public final int mPercent;
+    public final int mIconPresent;
+    public final boolean mUpdateObject;
+    public final boolean mUpdateExtras;
+    public Bitmap mIcon;
+    public InputStream mObjectStream;
+    public InputStream mExtrasStream;
+
+    public UpdateInfo(String shortName, String longName, String description, int objectMinor,
+                      int extrasMinor, int percent, int iconPresent, boolean updateO, boolean updateE)
+      {
+      mObjectShortName    = shortName;
+      mObjectLongName     = longName;
+      mDescription        = description;
+      mObjectMinorVersion = objectMinor;
+      mExtrasMinorVersion = extrasMinor;
+      mPercent            = percent;
+      mIconPresent        = iconPresent;
+      mUpdateObject       = updateO;
+      mUpdateExtras       = updateE;
+
+      mIcon = null;
+      }
+    }
+
+  private String mUrl;
+  private final ArrayList<UpdateInfo> mCompleted, mStarted;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public RubikUpdates()
+    {
+    mCompleted = new ArrayList<>();
+    mStarted   = new ArrayList<>();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private String debug(ArrayList<UpdateInfo> list)
+    {
+    String ret = "";
+
+    for( UpdateInfo info : list)
+      {
+      ret += (info.mObjectShortName+" "+info.mObjectLongName+" "+info.mDescription+" ");
+      ret += (info.mObjectMinorVersion+" "+info.mExtrasMinorVersion+" "+info.mUpdateObject+" "+info.mUpdateExtras+" , ");
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void parseLine(String[] elements)
+    {
+    String shortName   = elements[0].trim();
+    String objMinor    = elements[1].trim();
+    String extMinor    = elements[2].trim();
+    String percent     = elements[3].trim();
+    String iconPresent = elements[4].trim();
+    String longName    = elements[5];
+    String description = elements[6];
+    int oMinor, eMinor, oPercent, oIcon;
+
+    try { oMinor = Integer.parseInt(objMinor); }
+    catch (NumberFormatException ex) { oMinor = -1; }
+    try { eMinor = Integer.parseInt(extMinor); }
+    catch (NumberFormatException ex) { eMinor = -1; }
+    try { oPercent = Integer.parseInt(percent); }
+    catch (NumberFormatException ex) { oPercent = -1; }
+    try { oIcon = Integer.parseInt(iconPresent); }
+    catch (NumberFormatException ex) { oIcon = 0; }
+
+    if( oMinor>=0 && eMinor>=0 && oPercent>=0 )
+      {
+      int objOrdinal = RubikObjectList.getOrdinal(shortName.toUpperCase());
+      boolean updateO=true, updateE=true;
+
+      if( objOrdinal>=0 )
+        {
+        int localObjectMinor = RubikObjectList.getLocalObjectMinor(objOrdinal);
+        int localExtrasMinor = RubikObjectList.getLocalExtrasMinor(objOrdinal);
+        updateO = localObjectMinor<oMinor;
+        updateE = localExtrasMinor<eMinor;
+        }
+      if( updateO || updateE )
+        {
+        UpdateInfo info = new UpdateInfo(shortName,longName,description,oMinor,eMinor,oPercent,oIcon,updateO,updateE);
+        if(oPercent>=100) mCompleted.add(info);
+        else              mStarted.add(info);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void parse(String updates)
+    {
+    android.util.Log.e("D", updates);
+
+    mCompleted.clear();
+    mStarted.clear();
+
+    String[] lines = updates.split("\n");
+    int numLines = lines.length;
+
+    if( numLines>=1 )
+      {
+      mUrl = lines[0];
+      if( !mUrl.endsWith("/") ) mUrl += "/";
+
+      for(int line=1; line<numLines; line++)
+        {
+        String[] elements = lines[line].split(",");
+        if( elements.length>=7 ) parseLine(elements);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void updateDone(String shortName)
+    {
+    for( UpdateInfo info : mCompleted)
+      {
+      if( info.mObjectShortName.equals(shortName) )
+        {
+        mCompleted.remove(info);
+        return;
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public UpdateInfo getCompletedUpdate(int ordinal)
+    {
+    return mCompleted.get(ordinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public UpdateInfo getStartedUpdate(int ordinal)
+    {
+    return mStarted.get(ordinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCompletedNumber()
+    {
+    return mCompleted.size();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getStartedNumber()
+    {
+    return mStarted.size();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Bitmap getCompletedIcon(int ordinal)
+    {
+    return mCompleted.get(ordinal).mIcon;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public Bitmap getStartedIcon(int ordinal)
+    {
+    return mStarted.get(ordinal).mIcon;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCompletedIconPresent(int ordinal)
+    {
+    return mCompleted.get(ordinal).mIconPresent;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getStartedIconPresent(int ordinal)
+    {
+    return mStarted.get(ordinal).mIconPresent;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getCompletedURL(int ordinal)
+    {
+    UpdateInfo info = mCompleted.get(ordinal);
+    return info!=null ? mUrl + info.mObjectShortName + ".png" : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getStartedURL(int ordinal)
+    {
+    UpdateInfo info = mStarted.get(ordinal);
+    return info!=null ? mUrl + info.mObjectShortName + ".png" : null;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String getURL()
+    {
+    return mUrl;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setCompletedIcon(int ordinal, Bitmap icon)
+    {
+    UpdateInfo info = mCompleted.get(ordinal);
+    info.mIcon = icon;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setStartedIcon(int ordinal, Bitmap icon)
+    {
+    UpdateInfo info = mStarted.get(ordinal);
+    info.mIcon = icon;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void showDebug()
+    {
+    android.util.Log.e("D", "url: "+mUrl);
+    android.util.Log.e("D", "ready objects: "+debug(mCompleted));
+    android.util.Log.e("D", "next  objects: "+debug(mStarted));
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index c81af715..dc946271 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -53,8 +53,8 @@ import org.distorted.objectlib.effects.BaseEffect;
 
 import org.distorted.dialogs.RubikDialogError;
 import org.distorted.dialogs.RubikDialogPrivacy;
-import org.distorted.network.RubikScores;
-import org.distorted.network.RubikNetwork;
+import org.distorted.external.RubikScores;
+import org.distorted.external.RubikNetwork;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
 import org.distorted.screens.ScreenList;
diff --git a/src/main/java/org/distorted/main/RubikObjectLibInterface.java b/src/main/java/org/distorted/main/RubikObjectLibInterface.java
index 05e05dda..48d1aef0 100644
--- a/src/main/java/org/distorted/main/RubikObjectLibInterface.java
+++ b/src/main/java/org/distorted/main/RubikObjectLibInterface.java
@@ -34,7 +34,7 @@ import com.google.firebase.crashlytics.FirebaseCrashlytics;
 
 import org.distorted.library.message.EffectMessageSender;
 
-import org.distorted.network.RubikNetwork;
+import org.distorted.external.RubikNetwork;
 import org.distorted.objectlib.BuildConfig;
 import org.distorted.objectlib.helpers.BlockController;
 import org.distorted.objectlib.helpers.ObjectLibInterface;
@@ -42,7 +42,7 @@ import org.distorted.objectlib.main.ObjectControl;
 
 import org.distorted.dialogs.RubikDialogNewRecord;
 import org.distorted.dialogs.RubikDialogSolved;
-import org.distorted.network.RubikScores;
+import org.distorted.external.RubikScores;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
 import org.distorted.screens.RubikScreenPlay;
diff --git a/src/main/java/org/distorted/main/RubikRenderer.java b/src/main/java/org/distorted/main/RubikRenderer.java
index 74eea6fb..d86a94c3 100644
--- a/src/main/java/org/distorted/main/RubikRenderer.java
+++ b/src/main/java/org/distorted/main/RubikRenderer.java
@@ -29,7 +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 org.distorted.external.RubikNetwork;
 import org.distorted.objectlib.main.ObjectControl;
 
 import javax.microedition.khronos.egl.EGLConfig;
diff --git a/src/main/java/org/distorted/network/RubikNetwork.java b/src/main/java/org/distorted/network/RubikNetwork.java
deleted file mode 100644
index b4f96e59..00000000
--- a/src/main/java/org/distorted/network/RubikNetwork.java
+++ /dev/null
@@ -1,934 +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 java.io.BufferedReader;
-import java.io.IOException;
-import java.io.InputStream;
-import java.io.InputStreamReader;
-import java.net.HttpURLConnection;
-import java.net.URL;
-import java.net.UnknownHostException;
-import java.security.MessageDigest;
-import java.security.NoSuchAlgorithmException;
-
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
-import android.graphics.Bitmap;
-import android.graphics.BitmapFactory;
-
-import androidx.fragment.app.FragmentActivity;
-
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.objectlib.json.JsonWriter;
-import org.distorted.objects.RubikObjectList;
-
-import static org.distorted.objects.RubikObjectList.MAX_LEVEL;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikNetwork
-  {
-  public interface ScoresReceiver
-    {
-    void receive(String[][][] country, String[][][] name, float[][][] time);
-    void message(String mess);
-    void error(String error);
-    }
-
-  public interface IconReceiver
-    {
-    void iconDownloaded(int ordinal, Bitmap bitmap);
-    }
-
-  public interface Updatee
-    {
-    void receiveUpdate(RubikUpdates update);
-    void errorUpdate();
-    }
-
-  public interface Downloadee
-    {
-    void jsonDownloaded();
-    }
-
-  public static final int MAX_PLACES = 10;
-
-  private static final int REND_ADRENO= 0;
-  private static final int REND_MALI  = 1;
-  private static final int REND_POWER = 2;
-  private static final int REND_OTHER = 3;
-
-  private static final int DEBUG_RUNNING = 1;
-  private static final int DEBUG_SUCCESS = 2;
-  private static final int DEBUG_FAILURE = 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 String[][][] mCountry;
-  private static String[][][] mName;
-  private static float[][][] mTime;
-  private static int[][] mPlaces;
-
-  private static RubikNetwork mThis;
-  private static String mScores = "";
-  private static boolean mRunning = false;
-  private static Updatee mUpdatee;
-  private static String mVersion;
-  private static int mNumObjects;
-  private static RubikUpdates mUpdates;
-  private static int mDebugState;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private static void initializeStatics()
-    {
-    int newNum = RubikObjectList.getNumObjects();
-
-    if( mCountry==null || newNum!=mNumObjects ) mCountry = new String[newNum][MAX_LEVEL][MAX_PLACES];
-    if( mName==null    || newNum!=mNumObjects ) mName    = new String[newNum][MAX_LEVEL][MAX_PLACES];
-    if( mTime==null    || newNum!=mNumObjects ) mTime    = new  float[newNum][MAX_LEVEL][MAX_PLACES];
-    if( mPlaces==null  || newNum!=mNumObjects ) mPlaces  = new    int[newNum][MAX_LEVEL];
-
-    if( mUpdates==null ) mUpdates = new RubikUpdates();
-
-    mNumObjects = newNum;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 fillValuesNormal(ScoresReceiver receiver)
-    {
-    int begin=-1 ,end, len = mScores.length();
-    String row;
-
-    if( len==0 )
-      {
-      receiver.error("1");
-      return false;
-      }
-    else if( len<=2 )
-      {
-      receiver.error(mScores);
-      return false;
-      }
-
-    for(int i=0; i<mNumObjects; 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 = RubikObjectList.getOrdinal( row.substring(0,s1) );
-
-      if( object>=0 && object<mNumObjects )
-        {
-        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 int getRendererType(String renderer)
-    {
-    if( renderer.contains("Adreno")  ) return REND_ADRENO;
-    if( renderer.contains("Mali")    ) return REND_MALI;
-    if( renderer.contains("PowerVR") ) return REND_POWER;
-
-    return REND_OTHER;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String parseRenderer(final int type, String renderer)
-    {
-    if( type==REND_ADRENO || type==REND_POWER )
-      {
-      int lastSpace = renderer.lastIndexOf(' ');
-      String ret = renderer.substring(lastSpace+1);
-      return URLencode(ret);
-      }
-
-    if( type==REND_MALI )
-      {
-      int firstHyphen = renderer.indexOf('-');
-      String ret = renderer.substring(firstHyphen+1);
-      return URLencode(ret);
-      }
-
-    return "other";
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String parseVersion(final int type, String version)
-    {
-    switch(type)
-      {
-      case REND_ADRENO: int aMonkey = version.indexOf('@');
-                        int aDot = version.indexOf('.', aMonkey);
-                        String ret1 = aDot>=3 ? version.substring(aDot-3,aDot) : "";
-                        return URLencode(ret1);
-      case REND_MALI  : int mV1 = version.indexOf("v1");
-                        int mHyphen = version.indexOf('-', mV1);
-                        String ret2 = mHyphen>mV1+3 && mV1>=0 ? version.substring(mV1+3,mHyphen) : "";
-                        return URLencode(ret2);
-      case REND_POWER : int pMonkey = version.indexOf('@');
-                        int pSpace  = version.lastIndexOf(' ');
-                        String ret3 = pSpace>=0 && pMonkey>pSpace+1 ? version.substring(pSpace+1,pMonkey) : "";
-                        return URLencode(ret3);
-      default         : return "";
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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, ScoresReceiver receiver)
-    {
-    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();
-
-      InputStream is = conn.getInputStream();
-      BufferedReader r = new BufferedReader(new InputStreamReader(is));
-      StringBuilder total = new StringBuilder();
-
-      for (String line; (line = r.readLine()) != null; )
-        {
-        total.append(line).append('\n');
-        }
-
-      mScores = total.toString();
-      conn.disconnect();
-      }
-    catch( final UnknownHostException e )
-      {
-      receiver.message("No access to Internet");
-      return false;
-      }
-    catch( final SecurityException e )
-      {
-      receiver.message("Application not authorized to connect to the Internet");
-      return false;
-      }
-    catch( final Exception e )
-      {
-      receiver.message(e.getMessage());
-      return false;
-      }
-
-    if( mScores.length()==0 )
-      {
-      receiver.message("Failed to download scores");
-      return false;
-      }
-
-    return true;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String constructSuspiciousURL(String suspURL)
-    {
-    RubikScores scores = RubikScores.getInstance();
-    int deviceID= scores.getDeviceID();
-    String suspicious = URLencode(suspURL);
-
-    String url="https://distorted.org/magic/cgi-bin/suspicious.cgi";
-    url += "?i="+deviceID+"&d="+suspicious;
-
-    return url;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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();
-    int objectAPI   = JsonWriter.VERSION_OBJECT_MAJOR;
-    int tutorialAPI = JsonWriter.VERSION_EXTRAS_MAJOR;
-
-    renderer = URLencode(renderer);
-    version  = URLencode(version);
-
-    String url="https://distorted.org/magic/cgi-bin/debugs-new.cgi";
-    url += "?n="+name+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion+"d";
-    url += "&d="+renderer+"&v="+version+"&a="+objectAPI+"&b="+tutorialAPI;
-
-    return url;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String constructDownloadURL()
-    {
-    RubikScores scores = RubikScores.getInstance();
-    String name = URLencode(scores.getName());
-    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+"&r="+numRuns+"&p="+numPlay+"&c="+country+"&e="+mVersion;
-
-    return url;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String constructSubmitURL()
-    {
-    RubikScores scores = RubikScores.getInstance();
-    String name = URLencode(scores.getName());
-    String veri = scores.isVerified() ? "1" : "";
-    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 renderer = DistortedLibrary.getDriverRenderer();
-    String version  = DistortedLibrary.getDriverVersion();
-
-    int type = getRendererType(renderer);
-    renderer = parseRenderer(type,renderer);
-    version  = parseVersion(type,version);
-
-    String url1="https://distorted.org/magic/cgi-bin/submit.cgi";
-    String url2 = "n="+name+"&v="+veri+"&r="+numRuns+"&p="+numPlay+"&i="+deviceID+"&e="+mVersion;
-    url2 += "&d="+renderer+"&s="+version+reclist+"&c="+country+"&f="+epoch;
-    String hash = computeHash( url2, salt.getBytes() );
-
-    return url1 + "?" + url2 + "&h=" + hash;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean gottaDownload()
-    {
-    return ((mScores.length()==0) && !mRunning);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void figureOutVersion(FragmentActivity act)
-    {
-    try
-      {
-      PackageInfo pInfo = act.getPackageManager().getPackageInfo( act.getPackageName(), 0);
-      mVersion = pInfo.versionName;
-      }
-    catch (PackageManager.NameNotFoundException e)
-      {
-      mVersion = "0.9.2";
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void downloadThread(ScoresReceiver receiver)
-    {
-    try
-      {
-      if( gottaDownload() )
-        {
-        mRunning = true;
-        boolean receiveValues = network(constructDownloadURL(),receiver);
-
-        if( mRunning )
-          {
-          receiveValues = fillValuesNormal(receiver);
-          mRunning = false;
-          }
-
-        if( receiveValues ) receiver.receive(mCountry, mName, mTime);
-        }
-      }
-    catch( Exception e )
-      {
-      receiver.message("Exception downloading records: "+e.getMessage() );
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void submitThread(ScoresReceiver receiver)
-    {
-    try
-      {
-      mRunning = true;
-      RubikScores scores = RubikScores.getInstance();
-
-      if( scores.thereAreUnsubmittedRecords() )
-        {
-        boolean receiveValues = network(constructSubmitURL(),receiver);
-
-        if( mRunning )
-          {
-          receiveValues = fillValuesNormal(receiver);
-          mRunning = false;
-          }
-
-        if( receiveValues )
-          {
-          RubikScores.getInstance().successfulSubmit();
-          receiver.receive(mCountry, mName, mTime);
-          }
-        }
-      }
-    catch( Exception e )
-      {
-      receiver.message("Exception submitting records: "+e.getMessage() );
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void debugThread()
-    {
-    String url = constructDebugURL();
-/*
-    try { Thread.sleep(5000); }
-    catch( InterruptedException ignored) {}
-*/
-    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();
-
-      InputStream is = conn.getInputStream();
-      BufferedReader r = new BufferedReader(new InputStreamReader(is));
-      StringBuilder answer = new StringBuilder();
-
-      for (String line; (line = r.readLine()) != null; )
-        {
-        answer.append(line).append('\n');
-        }
-
-      String updates = answer.toString();
-      conn.disconnect();
-      mUpdates.parse(updates);
-
-      if( mUpdatee!=null ) mUpdatee.receiveUpdate(mUpdates);
-      mDebugState = DEBUG_SUCCESS;
-      }
-    catch( final Exception e )
-      {
-      if( mUpdatee!=null ) mUpdatee.errorUpdate();
-      mDebugState = DEBUG_FAILURE;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void suspiciousThread(String suspURL)
-    {
-    String url = constructSuspiciousURL(suspURL);
-
-    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();
-      conn.disconnect();
-      }
-    catch( final Exception e )
-      {
-      // ignore
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Bitmap downloadIcon(String url)
-    {
-    try
-      {
-      java.net.URL connectURL = new URL(url);
-      HttpURLConnection conn = (HttpURLConnection) connectURL.openConnection();
-      conn.setDoInput(true);
-      conn.connect();
-      InputStream input = conn.getInputStream();
-      Bitmap icon = BitmapFactory.decodeStream(input);
-      conn.disconnect();
-      return icon;
-      }
-    catch (IOException e)
-      {
-      android.util.Log.e("D", "Failed to download "+url);
-      android.util.Log.e("D", e.getMessage() );
-      return null;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void iconThread(IconReceiver receiver)
-    {
-    int numC = mUpdates.getCompletedNumber();
-    int numS = mUpdates.getStartedNumber();
-
-    for(int c=0; c<numC; c++)
-      {
-      int iconPresent = mUpdates.getCompletedIconPresent(c);
-
-      if( iconPresent!=0 )
-        {
-        Bitmap icon = mUpdates.getCompletedIcon(c);
-
-        if( icon==null )
-          {
-          String url = mUpdates.getCompletedURL(c);
-          icon = downloadIcon(url);
-          }
-        if( icon!=null )
-          {
-          mUpdates.setCompletedIcon(c,icon);
-          receiver.iconDownloaded(c,icon);
-          }
-        }
-      }
-
-    for(int s=0; s<numS; s++)
-      {
-      int iconPresent = mUpdates.getStartedIconPresent(s);
-
-      if( iconPresent!=0 )
-        {
-        Bitmap icon = mUpdates.getStartedIcon(s);
-
-        if( icon==null )
-          {
-          String url = mUpdates.getStartedURL(s);
-          icon = downloadIcon(url);
-          }
-        if( icon!=null )
-          {
-          mUpdates.setStartedIcon(s,icon);
-          receiver.iconDownloaded(numC+s,icon);
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private InputStream downloadJSON(String name)
-    {
-    String url = mUpdates.getURL() + name;
-
-    try
-      {
-      java.net.URL connectURL = new URL(url);
-      HttpURLConnection conn = (HttpURLConnection) connectURL.openConnection();
-      conn.setDoInput(true);
-      conn.connect();
-      InputStream stream = conn.getInputStream();
-      conn.disconnect();
-      return stream;
-      }
-    catch (IOException e)
-      {
-      android.util.Log.e("D", "Failed to download "+url);
-      android.util.Log.e("D", e.getMessage() );
-      return null;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void jsonThread(final RubikUpdates.UpdateInfo info, Downloadee downloadee)
-    {
-    if(info.mUpdateObject) info.mObjectStream = downloadJSON(info.mObjectShortName+"_object.json");
-    if(info.mUpdateExtras) info.mExtrasStream = downloadJSON(info.mObjectShortName+"_extras.json");
-
-    downloadee.jsonDownloaded();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private RubikNetwork()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void onPause()
-    {
-    mRunning = false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static RubikNetwork getInstance()
-    {
-    if( mThis==null )
-      {
-      mThis = new RubikNetwork();
-      }
-
-    return mThis;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void download(final ScoresReceiver receiver, final FragmentActivity act)
-    {
-    initializeStatics();
-    figureOutVersion(act);
-
-    Thread thread = new Thread()
-      {
-      public void run()
-        {
-        downloadThread(receiver);
-        }
-      };
-
-    thread.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void submit(ScoresReceiver receiver, final FragmentActivity act)
-    {
-    initializeStatics();
-    figureOutVersion(act);
-
-    Thread thread = new Thread()
-      {
-      public void run()
-        {
-        submitThread(receiver);
-        }
-      };
-
-    thread.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void debug()
-    {
-    initializeStatics();
-    mDebugState = DEBUG_RUNNING;
-
-    Thread thread = new Thread()
-      {
-      public void run()
-        {
-        debugThread();
-        }
-      };
-
-    thread.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void suspicious(final String suspicious)
-    {
-    initializeStatics();
-
-    Thread thread = new Thread()
-      {
-      public void run()
-        {
-        suspiciousThread(suspicious);
-        }
-      };
-
-    thread.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Yes it can happen that the second Updatee registers before we sent an update to the first one
-// and, as a result, the update never gets sent to the first one. This is not a problem (now, when
-// there are only two updatees - the RubikStatePlay and the UpdateDialog)
-//
-// Yes, there is also a remote possibility that the two threads executing this function and executing
-// the sendDebug() get swapped exactly in unlucky moment and the update never gets to the updatee.
-// We don't care about such remote possibility, then the app simply would signal that there are no
-// updates available.
-
-  public void signUpForUpdates(Updatee updatee)
-    {
-         if( mDebugState==DEBUG_SUCCESS ) updatee.receiveUpdate(mUpdates);
-    else if( mDebugState==DEBUG_FAILURE ) updatee.errorUpdate();
-    else mUpdatee = updatee;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void downloadIcons(final IconReceiver receiver)
-    {
-    initializeStatics();
-
-    Thread thread = new Thread()
-      {
-      public void run()
-        {
-        iconThread(receiver);
-        }
-      };
-
-    thread.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void downloadJSON(final RubikUpdates.UpdateInfo info, final Downloadee downloadee)
-    {
-    initializeStatics();
-
-    Thread thread = new Thread()
-      {
-      public void run()
-        {
-        jsonThread(info,downloadee);
-        }
-      };
-
-    thread.start();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void updateDone(String shortName)
-    {
-    mUpdates.updateDone(shortName);
-    }
-}
\ 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
deleted file mode 100644
index 64db538d..00000000
--- a/src/main/java/org/distorted/network/RubikScores.java
+++ /dev/null
@@ -1,458 +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.network;
-
-import java.util.HashMap;
-import java.util.UUID;
-
-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.RubikObject;
-import org.distorted.objects.RubikObjectList;
-
-import static org.distorted.objects.RubikObjectList.MAX_LEVEL;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// hold my own scores, and some other statistics.
-
-public class RubikScores
-  {
-  public static final int MULT = 1000000;
-  public static final long NO_RECORD = Long.MAX_VALUE;
-  private static RubikScores mThis;
-
-  private String mName, mCountry;
-  private boolean mNameIsVerified;
-  private int mNumRuns;
-  private int mNumPlays;
-  private int mNumWins;
-  private int mDeviceID;
-
-  private static class MapValue
-    {
-    long record;
-    boolean submitted;
-
-    MapValue(long rec,int sub)
-      {
-      record    = rec;
-      submitted = sub!=0;
-      }
-    }
-
-  private final HashMap<Integer,MapValue> mMap;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private RubikScores()
-    {
-    mMap = new HashMap<>();
-
-    mName = "";
-    mCountry = "un";
-
-    mNameIsVerified = false;
-
-    mNumPlays= -1;
-    mNumRuns = -1;
-    mDeviceID= -1;
-    mNumWins =  0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int mapKey(int object,int level)
-    {
-    return object*MULT + level;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 key: mMap.keySet())
-      {
-      MapValue value = mMap.get(key);
-      if( value!=null ) value.submitted = true;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getDeviceID()
-    {
-    return mDeviceID;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized boolean thereAreUnsubmittedRecords()
-    {
-    for(int key: mMap.keySet())
-      {
-      MapValue value = mMap.get(key);
-      if( value!=null && !value.submitted && value.record<NO_RECORD) return true;
-      }
-
-    return false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  synchronized String getRecordList(String strObj, String strLvl, String strTim)
-    {
-    StringBuilder builderObj = new StringBuilder();
-    StringBuilder builderLvl = new StringBuilder();
-    StringBuilder builderTim = new StringBuilder();
-    boolean first = true;
-
-    for(int key: mMap.keySet())
-      {
-      MapValue value = mMap.get(key);
-
-      if( value!=null && !value.submitted && value.record<NO_RECORD)
-        {
-        if( !first )
-          {
-          builderObj.append(',');
-          builderLvl.append(',');
-          builderTim.append(',');
-          }
-        first=false;
-
-        RubikObject object = RubikObjectList.getObject(key/MULT);
-
-        if( object!=null )
-          {
-          builderObj.append(object.getName());
-          builderLvl.append(key%MULT);
-          builderTim.append(value.record);
-          }
-        }
-      }
-
-    return strObj+builderObj.toString()+strLvl+builderLvl.toString()+strTim+builderTim.toString();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Public API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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 setRecord(int object, int level, long record)
-    {
-    int key = mapKey(object,level)-1; // -1 - historical reasons; previous versions saved it like this.
-    MapValue oldValue = mMap.get(key);
-
-    if( oldValue==null )
-      {
-      MapValue value = new MapValue(record,0);
-      mMap.put(key,value);
-      return true;
-      }
-
-    long oldRecord = oldValue.record;
-
-    if( oldRecord>record)
-      {
-      MapValue value = new MapValue(record,0);
-      mMap.put(key,value);
-      return true;
-      }
-
-    return false;
-    }
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized long getRecord(int object, int level)
-    {
-    int key = mapKey(object,level);
-    MapValue value = mMap.get(key);
-    return value!=null ? value.record : NO_RECORD;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public synchronized boolean isSolved(int object, int level)
-    {
-    int key = mapKey(object,level);
-    MapValue value = mMap.get(key);
-    return value!=null && value.record<NO_RECORD;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  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)
-    {
-    int numObjects = RubikObjectList.getNumObjects();
-    StringBuilder builder = new StringBuilder();
-
-    for(int level=0; level<MAX_LEVEL; level++)
-      {
-      builder.setLength(0);
-
-      for(int object=0; object<numObjects; object++)
-        {
-        int key = mapKey(object,level);
-        RubikObject obj = RubikObjectList.getObject(object);
-        MapValue value = mMap.get(key);
-
-        if( obj!=null && value!=null && value.record<NO_RECORD )
-          {
-          builder.append(obj.getName());
-          builder.append("=");
-          builder.append(value.record);
-          builder.append(",");
-          builder.append(value.submitted ? 1:0 );
-          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, timeStr, submStr, errorStr="";
-    int start, end, equals, comma, object, subm;
-    long time;
-    boolean thereWasError = false;
-    int numObjects = RubikObjectList.getNumObjects();
-
-    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;
-
-        equals = subStr.indexOf("=");
-        comma  = subStr.indexOf(",");
-
-        if( equals>=0 && comma>=0 )
-          {
-          nameStr = subStr.substring(0,equals);
-          timeStr = subStr.substring(equals+1,comma);
-          submStr = subStr.substring(comma+1);
-
-          object = RubikObjectList.getOrdinal(nameStr);
-
-          if( object>=0 && object<numObjects )
-            {
-            time = Long.parseLong(timeStr);
-            subm = Integer.parseInt(submStr);
-
-            if( subm>=0 && subm<=1 )
-              {
-              MapValue value = new MapValue(time,subm);
-              int key = mapKey(object,level);
-              mMap.put(key,value);
-              }
-            else
-              {
-              errorStr += ("error1: subm="+subm+" obj: "+nameStr+"\n");
-              thereWasError= true;
-              }
-            }
-          else
-            {
-            errorStr += ("error2: object="+object+" obj: "+nameStr+"\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);
-      }
-    }
-  }
diff --git a/src/main/java/org/distorted/network/RubikUpdates.java b/src/main/java/org/distorted/network/RubikUpdates.java
deleted file mode 100644
index 070e6294..00000000
--- a/src/main/java/org/distorted/network/RubikUpdates.java
+++ /dev/null
@@ -1,274 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2022 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 java.io.InputStream;
-import java.util.ArrayList;
-import android.graphics.Bitmap;
-import org.distorted.objects.RubikObjectList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class RubikUpdates
-{
-  public static class UpdateInfo
-    {
-    public final String mObjectShortName;
-    public final String mObjectLongName;
-    public final String mDescription;
-    public final int mObjectMinorVersion;
-    public final int mExtrasMinorVersion;
-    public final int mPercent;
-    public final int mIconPresent;
-    public final boolean mUpdateObject;
-    public final boolean mUpdateExtras;
-    public Bitmap mIcon;
-    public InputStream mObjectStream;
-    public InputStream mExtrasStream;
-
-    public UpdateInfo(String shortName, String longName, String description, int objectMinor,
-                      int extrasMinor, int percent, int iconPresent, boolean updateO, boolean updateE)
-      {
-      mObjectShortName    = shortName;
-      mObjectLongName     = longName;
-      mDescription        = description;
-      mObjectMinorVersion = objectMinor;
-      mExtrasMinorVersion = extrasMinor;
-      mPercent            = percent;
-      mIconPresent        = iconPresent;
-      mUpdateObject       = updateO;
-      mUpdateExtras       = updateE;
-
-      mIcon = null;
-      }
-    }
-
-  private String mUrl;
-  private final ArrayList<UpdateInfo> mCompleted, mStarted;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public RubikUpdates()
-    {
-    mCompleted = new ArrayList<>();
-    mStarted   = new ArrayList<>();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private String debug(ArrayList<UpdateInfo> list)
-    {
-    String ret = "";
-
-    for( UpdateInfo info : list)
-      {
-      ret += (info.mObjectShortName+" "+info.mObjectLongName+" "+info.mDescription+" ");
-      ret += (info.mObjectMinorVersion+" "+info.mExtrasMinorVersion+" "+info.mUpdateObject+" "+info.mUpdateExtras+" , ");
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void parseLine(String[] elements)
-    {
-    String shortName   = elements[0].trim();
-    String objMinor    = elements[1].trim();
-    String extMinor    = elements[2].trim();
-    String percent     = elements[3].trim();
-    String iconPresent = elements[4].trim();
-    String longName    = elements[5];
-    String description = elements[6];
-    int oMinor, eMinor, oPercent, oIcon;
-
-    try { oMinor = Integer.parseInt(objMinor); }
-    catch (NumberFormatException ex) { oMinor = -1; }
-    try { eMinor = Integer.parseInt(extMinor); }
-    catch (NumberFormatException ex) { eMinor = -1; }
-    try { oPercent = Integer.parseInt(percent); }
-    catch (NumberFormatException ex) { oPercent = -1; }
-    try { oIcon = Integer.parseInt(iconPresent); }
-    catch (NumberFormatException ex) { oIcon = 0; }
-
-    if( oMinor>=0 && eMinor>=0 && oPercent>=0 )
-      {
-      int objOrdinal = RubikObjectList.getOrdinal(shortName.toUpperCase());
-      boolean updateO=true, updateE=true;
-
-      if( objOrdinal>=0 )
-        {
-        int localObjectMinor = RubikObjectList.getLocalObjectMinor(objOrdinal);
-        int localExtrasMinor = RubikObjectList.getLocalExtrasMinor(objOrdinal);
-        updateO = localObjectMinor<oMinor;
-        updateE = localExtrasMinor<eMinor;
-        }
-      if( updateO || updateE )
-        {
-        UpdateInfo info = new UpdateInfo(shortName,longName,description,oMinor,eMinor,oPercent,oIcon,updateO,updateE);
-        if(oPercent>=100) mCompleted.add(info);
-        else              mStarted.add(info);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void parse(String updates)
-    {
-    android.util.Log.e("D", updates);
-
-    mCompleted.clear();
-    mStarted.clear();
-
-    String[] lines = updates.split("\n");
-    int numLines = lines.length;
-
-    if( numLines>=1 )
-      {
-      mUrl = lines[0];
-      if( !mUrl.endsWith("/") ) mUrl += "/";
-
-      for(int line=1; line<numLines; line++)
-        {
-        String[] elements = lines[line].split(",");
-        if( elements.length>=7 ) parseLine(elements);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void updateDone(String shortName)
-    {
-    for( UpdateInfo info : mCompleted)
-      {
-      if( info.mObjectShortName.equals(shortName) )
-        {
-        mCompleted.remove(info);
-        return;
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public UpdateInfo getCompletedUpdate(int ordinal)
-    {
-    return mCompleted.get(ordinal);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public UpdateInfo getStartedUpdate(int ordinal)
-    {
-    return mStarted.get(ordinal);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCompletedNumber()
-    {
-    return mCompleted.size();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getStartedNumber()
-    {
-    return mStarted.size();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Bitmap getCompletedIcon(int ordinal)
-    {
-    return mCompleted.get(ordinal).mIcon;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public Bitmap getStartedIcon(int ordinal)
-    {
-    return mStarted.get(ordinal).mIcon;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCompletedIconPresent(int ordinal)
-    {
-    return mCompleted.get(ordinal).mIconPresent;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getStartedIconPresent(int ordinal)
-    {
-    return mStarted.get(ordinal).mIconPresent;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getCompletedURL(int ordinal)
-    {
-    UpdateInfo info = mCompleted.get(ordinal);
-    return info!=null ? mUrl + info.mObjectShortName + ".png" : null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getStartedURL(int ordinal)
-    {
-    UpdateInfo info = mStarted.get(ordinal);
-    return info!=null ? mUrl + info.mObjectShortName + ".png" : null;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String getURL()
-    {
-    return mUrl;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setCompletedIcon(int ordinal, Bitmap icon)
-    {
-    UpdateInfo info = mCompleted.get(ordinal);
-    info.mIcon = icon;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setStartedIcon(int ordinal, Bitmap icon)
-    {
-    UpdateInfo info = mStarted.get(ordinal);
-    info.mIcon = icon;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void showDebug()
-    {
-    android.util.Log.e("D", "url: "+mUrl);
-    android.util.Log.e("D", "ready objects: "+debug(mCompleted));
-    android.util.Log.e("D", "next  objects: "+debug(mStarted));
-    }
-}
\ No newline at end of file
diff --git a/src/main/java/org/distorted/screens/RubikScreenPlay.java b/src/main/java/org/distorted/screens/RubikScreenPlay.java
index 5708ff4f..d24645a8 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPlay.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPlay.java
@@ -41,8 +41,8 @@ import android.widget.RelativeLayout;
 import android.widget.TextView;
 
 import org.distorted.dialogs.RubikDialogUpdates;
-import org.distorted.network.RubikNetwork;
-import org.distorted.network.RubikUpdates;
+import org.distorted.external.RubikNetwork;
+import org.distorted.external.RubikUpdates;
 import org.distorted.objectlib.main.ObjectControl;
 
 import org.distorted.main.R;
@@ -53,7 +53,7 @@ import org.distorted.dialogs.RubikDialogScores;
 import org.distorted.dialogs.RubikDialogTutorial;
 import org.distorted.helpers.TransparentButton;
 import org.distorted.helpers.TransparentImageButton;
-import org.distorted.network.RubikScores;
+import org.distorted.external.RubikScores;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolving.java b/src/main/java/org/distorted/screens/RubikScreenSolving.java
index 1de6c140..7b4650f3 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolving.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolving.java
@@ -33,7 +33,7 @@ import org.distorted.dialogs.RubikDialogAbandon;
 import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.network.RubikScores;
+import org.distorted.external.RubikScores;
 import org.distorted.objects.RubikObjectList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
