commit 7e9d918bf7d26a965bfa1c658f895ed23c51db59
Author: leszek <leszek@koltunski.pl>
Date:   Mon Dec 30 11:27:15 2024 +0100

    minor

diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index cd4bb591..6ec11704 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -32,9 +32,9 @@
         <activity android:name="org.distorted.tutorials.TutorialActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.info.InfoActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.bandaged.BandagedActivity" android:exported="false" android:screenOrientation="portrait"/>
-        <activity android:name="org.distorted.solverui.SolverActivity" android:exported="false" android:screenOrientation="portrait"/>
-        <activity android:name="org.distorted.patternui.PatternActivity" android:exported="false" android:screenOrientation="portrait"/>
-        <activity android:name="org.distorted.playui.PlayActivity" android:exported="false" android:screenOrientation="portrait"/>
+        <activity android:name="org.distorted.solvers.SolverActivity" android:exported="false" android:screenOrientation="portrait"/>
+        <activity android:name="org.distorted.patterns.PatternActivity" android:exported="false" android:screenOrientation="portrait"/>
+        <activity android:name="org.distorted.play.PlayActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.config.ConfigActivity" android:exported="false" android:screenOrientation="portrait"/>
 
         <service
diff --git a/src/main/java/org/distorted/bandaged/BandagedActivity.java b/src/main/java/org/distorted/bandaged/BandagedActivity.java
index 5ff945ef..11c2911d 100644
--- a/src/main/java/org/distorted/bandaged/BandagedActivity.java
+++ b/src/main/java/org/distorted/bandaged/BandagedActivity.java
@@ -31,7 +31,7 @@ import org.distorted.objectlib.main.InitAssets;
 import org.distorted.objectlib.main.TwistyJson;
 import org.distorted.objectlib.main.TwistyObject;
 import org.distorted.os.OSInterface;
-import org.distorted.playui.PlayActivity;
+import org.distorted.play.PlayActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogAbandon.java b/src/main/java/org/distorted/dialogs/DialogAbandon.java
index 2e1e1380..9590fbd0 100644
--- a/src/main/java/org/distorted/dialogs/DialogAbandon.java
+++ b/src/main/java/org/distorted/dialogs/DialogAbandon.java
@@ -16,8 +16,8 @@ import android.widget.TextView;
 import androidx.fragment.app.FragmentActivity;
 
 import org.distorted.main.R;
-import org.distorted.playui.PlayActivity;
-import org.distorted.playui.ScreenList;
+import org.distorted.play.PlayActivity;
+import org.distorted.play.ScreenList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogNewRecord.java b/src/main/java/org/distorted/dialogs/DialogNewRecord.java
index ac4104e4..5a10989c 100644
--- a/src/main/java/org/distorted/dialogs/DialogNewRecord.java
+++ b/src/main/java/org/distorted/dialogs/DialogNewRecord.java
@@ -19,7 +19,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.helpers.RubikScores;
-import org.distorted.playui.PlayActivity;
+import org.distorted.play.PlayActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogPattern.java b/src/main/java/org/distorted/dialogs/DialogPattern.java
index bd140f7b..ec550d39 100644
--- a/src/main/java/org/distorted/dialogs/DialogPattern.java
+++ b/src/main/java/org/distorted/dialogs/DialogPattern.java
@@ -23,9 +23,9 @@ import org.distorted.objectlib.patterns.RubikPattern;
 import org.distorted.objectlib.patterns.RubikPatternList;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
-import org.distorted.patternui.PatternActivity;
-import org.distorted.patternui.ScreenList;
-import org.distorted.patternui.ScreenPattern;
+import org.distorted.patterns.PatternActivity;
+import org.distorted.patterns.ScreenList;
+import org.distorted.patterns.ScreenPattern;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogSetName.java b/src/main/java/org/distorted/dialogs/DialogSetName.java
index b537db27..4553feef 100644
--- a/src/main/java/org/distorted/dialogs/DialogSetName.java
+++ b/src/main/java/org/distorted/dialogs/DialogSetName.java
@@ -24,7 +24,7 @@ import android.widget.TextView;
 
 import org.distorted.main.R;
 import org.distorted.helpers.RubikScores;
-import org.distorted.playui.PlayActivity;
+import org.distorted.play.PlayActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogSolved.java b/src/main/java/org/distorted/dialogs/DialogSolved.java
index 35ff33fc..98d8d218 100644
--- a/src/main/java/org/distorted/dialogs/DialogSolved.java
+++ b/src/main/java/org/distorted/dialogs/DialogSolved.java
@@ -17,7 +17,7 @@ import android.view.View;
 import android.widget.TextView;
 
 import org.distorted.main.R;
-import org.distorted.playui.PlayActivity;
+import org.distorted.play.PlayActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogSolverView.java b/src/main/java/org/distorted/dialogs/DialogSolverView.java
index dbe16f9e..c1d58e28 100644
--- a/src/main/java/org/distorted/dialogs/DialogSolverView.java
+++ b/src/main/java/org/distorted/dialogs/DialogSolverView.java
@@ -20,9 +20,9 @@ import org.distorted.objectlib.helpers.OperatingSystemInterface;
 import org.distorted.objectlib.main.TwistyObject;
 import org.distorted.objectlib.solvers.verifiers.SolverAbstract;
 import org.distorted.objectlib.solvers.verifiers.SolvingList;
-import org.distorted.solverui.ScreenList;
-import org.distorted.solverui.ScreenSetupPosition;
-import org.distorted.solverui.SolverActivity;
+import org.distorted.solvers.ScreenList;
+import org.distorted.solvers.ScreenSetupPosition;
+import org.distorted.solvers.SolverActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
diff --git a/src/main/java/org/distorted/dialogs/DialogSolvers.java b/src/main/java/org/distorted/dialogs/DialogSolvers.java
index 6ffe1a6c..410d6b05 100644
--- a/src/main/java/org/distorted/dialogs/DialogSolvers.java
+++ b/src/main/java/org/distorted/dialogs/DialogSolvers.java
@@ -22,7 +22,7 @@ import androidx.fragment.app.FragmentActivity;
 
 import org.distorted.main.R;
 import org.distorted.objectlib.metadata.ListObjects;
-import org.distorted.solverui.SolverActivity;
+import org.distorted.solvers.SolverActivity;
 import org.distorted.objectlib.solvers.verifiers.SolvingList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/main/MainActivity.java b/src/main/java/org/distorted/main/MainActivity.java
index 671c4550..81b83f70 100644
--- a/src/main/java/org/distorted/main/MainActivity.java
+++ b/src/main/java/org/distorted/main/MainActivity.java
@@ -42,9 +42,9 @@ import org.distorted.helpers.RubikUpdates;
 import org.distorted.messaging.RubikInAppMessanging;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
-import org.distorted.patternui.PatternActivity;
-import org.distorted.playui.PlayActivity;
-import org.distorted.solverui.SolverActivity;
+import org.distorted.patterns.PatternActivity;
+import org.distorted.play.PlayActivity;
+import org.distorted.solvers.SolverActivity;
 import org.distorted.tutorials.TutorialActivity;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/patterns/PatternActivity.java b/src/main/java/org/distorted/patterns/PatternActivity.java
new file mode 100644
index 00000000..e0bbbd26
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/PatternActivity.java
@@ -0,0 +1,204 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.DisplayCutout;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.distorted.dialogs.DialogError;
+import org.distorted.helpers.BaseActivity;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.main.R;
+import org.distorted.objectlib.main.InitAssets;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.os.OSInterface;
+
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternActivity extends BaseActivity
+{
+    public static final float RATIO_UPP       = 0.100f;
+    public static final float PADDING         = 0.010f;
+    public static final float SMALL_MARGIN    = 0.004f;
+    public static final float BUTTON_TEXT_SIZE= 0.050f;
+    public static final float TITLE_TEXT_SIZE = 0.045f;
+
+    private static final int ACTIVITY_NUMBER = 5;
+    private static final float RATIO_INSET= 0.09f;
+
+    private int mObjectOrdinal;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle savedState)
+      {
+      super.onCreate(savedState);
+      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
+      setContentView(R.layout.pattern);
+
+      Bundle b = getIntent().getExtras();
+      mObjectOrdinal = b!=null ? b.getInt("obj") : 0;
+
+      computeScreenDimensions();
+      hideNavigationBar();
+      cutoutHack();
+      computeLowerBarHeight(RATIO_BAR);
+      computeUpperBarHeight(RATIO_UPP);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onAttachedToWindow()
+      {
+      super.onAttachedToWindow();
+
+      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
+        {
+        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
+        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
+
+        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
+        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
+        paramsHid.height = (int)(insetHeight*RATIO_INSET);
+        layoutHid.setLayoutParams(paramsHid);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      super.onPause();
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      view.onPause();
+      DistortedLibrary.onPause(ACTIVITY_NUMBER);
+      savePreferences();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      DistortedLibrary.onResume(ACTIVITY_NUMBER);
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      view.onResume();
+
+      createObject();
+      restorePreferences();
+      ScreenList.setScreen(this);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
+      super.onDestroy();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void savePreferences()
+      {
+      SharedPreferences.Editor editor = mPreferences.edit();
+
+      for(int i=0; i< ScreenList.LENGTH; i++ )
+        ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
+
+      ScreenList.savePreferences(editor);
+
+      boolean success = editor.commit();
+      if( !success ) android.util.Log.e("D", "Failed to save preferences");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void restorePreferences()
+      {
+      for (int i=0; i<ScreenList.LENGTH; i++)
+        ScreenList.getScreen(i).getScreenClass().restorePreferences(mPreferences);
+
+      ScreenList.restorePreferences(mPreferences);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void OpenGLError()
+      {
+      DialogError errDiag = new DialogError();
+      errDiag.show(getSupportFragmentManager(), null);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyObject getObject()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      return view.getObjectControl().getObject();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public DistortedScreen getScreen()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      return view.getRenderer().getScreen();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public ObjectControl getControl()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      return view.getObjectControl();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getObjectOrdinal()
+      {
+      return mObjectOrdinal;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void createObject()
+      {
+      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
+      ObjectControl control = view.getObjectControl();
+      RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+      int iconMode  = TwistyObject.MODE_NORM;
+      InputStream jsonStream = object==null ? null : object.getObjectStream(this);
+      InputStream meshStream = object==null ? null : object.getMeshStream(this);
+      String name = object==null ? "NULL" : object.getUpperName();
+      OSInterface os = view.getInterface();
+      InitAssets asset = new InitAssets(jsonStream,meshStream,os);
+
+      control.changeIfDifferent(mObjectOrdinal,name,iconMode,asset);
+      }
+}
diff --git a/src/main/java/org/distorted/patterns/PatternObjectLibInterface.java b/src/main/java/org/distorted/patterns/PatternObjectLibInterface.java
new file mode 100644
index 00000000..a4a73e37
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/PatternObjectLibInterface.java
@@ -0,0 +1,153 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.message.EffectMessageSender;
+import org.distorted.main.BuildConfig;
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.ObjectLibInterface;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternObjectLibInterface implements ObjectLibInterface
+{
+  PatternObjectLibInterface(PatternActivity act) { }
+  public void onScrambleEffectFinished() { }
+  public void onRemoveRotation(int axis, int row, int angle) { }
+  public void onBeginRotation() { }
+  public void failedToDrag() { }
+  public void onSolved() { }
+  public void onObjectCreated(long time) { }
+  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
+  public void onReplaceModeDown(int cubit, int face) { }
+  public void onReplaceModeUp() { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportProblem(String problem, boolean recordException)
+    {
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("libInterface", problem);
+      }
+    else
+      {
+      if( recordException )
+        {
+        Exception ex = new Exception(problem);
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.setCustomKey("problem" , problem);
+        crashlytics.recordException(ex);
+        }
+      else
+        {
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.log(problem);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportScramblingProblem(int place, long pause, long resume, long time)
+    {
+    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportRotationProblem(int place, long pause, long resume, long time)
+    {
+    String error = "ROTATION BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume);
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportThreadProblem(int place, long pause, long resume, long time)
+    {
+    String error = EffectMessageSender.reportState();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause  );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
+    {
+    switch(type)
+      {
+      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportJSONError(String error, int ordinal)
+    {
+    RubikObject object = RubikObjectList.getObject(ordinal);
+    String name = object==null ? "NULL" : object.getUpperName();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("name" , name );
+      crashlytics.setCustomKey("JSONerror", error );
+      crashlytics.recordException(ex);
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/patterns/PatternRenderer.java b/src/main/java/org/distorted/patterns/PatternRenderer.java
new file mode 100644
index 00000000..4c92d68e
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/PatternRenderer.java
@@ -0,0 +1,159 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import android.content.res.Resources;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.main.InternalOutputSurface;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.main.BuildConfig;
+import org.distorted.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectControl;
+
+import java.io.InputStream;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
+{
+   private final PatternSurfaceView mView;
+   private final Resources mResources;
+   private final DistortedScreen mScreen;
+   private final ObjectControl mControl;
+   private boolean mErrorShown;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   PatternRenderer(PatternSurfaceView v)
+     {
+     mView = v;
+     mResources = v.getResources();
+
+     mErrorShown = false;
+     mControl = v.getObjectControl();
+     mScreen = new DistortedScreen();
+
+     PatternActivity act = (PatternActivity)v.getContext();
+     act.setUpBackgroundColor(mScreen);
+
+     mScreen.enableDepthStencil(InternalOutputSurface.DEPTH_NO_STENCIL);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedScreen getScreen()
+     {
+     return mScreen;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// various things are done here delayed, 'after the next render' as not to be done mid-render and
+// cause artifacts.
+
+   @Override
+   public void onDrawFrame(GL10 glUnused)
+     {
+     long time = System.currentTimeMillis();
+     mControl.preRender();
+     mScreen.render(time);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceChanged(GL10 glUnused, int width, int height)
+      {
+      mScreen.resize(width,height);
+      mView.setScreenSize(width,height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
+      {
+      DistortedLibrary.setMax(EffectType.VERTEX,ObjectControl.MAX_QUATS+1);
+      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
+
+      VertexEffectRotate.enable();
+      VertexEffectQuaternion.enable();
+      BaseEffect.Type.enableEffects();
+
+      DistortedLibrary.onSurfaceCreated(this,1);
+      DistortedLibrary.setCull(true);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void distortedException(Exception ex)
+     {
+     String message = ex.getMessage();
+     String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+     String version = GLES30.glGetString(GLES30.GL_VERSION);
+     String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+     String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+     if( message==null ) message = "exception NULL";
+
+     if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("DISTORTED", message );
+       android.util.Log.e("DISTORTED", "GLSL Version "+shading);
+       android.util.Log.e("DISTORTED", "GL Version "  +version);
+       android.util.Log.e("DISTORTED", "GL Vendor "   +vendor);
+       android.util.Log.e("DISTORTED", "GL Renderer " +renderer);
+       }
+     else
+       {
+       FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+       crashlytics.setCustomKey("DistortedError", message );
+       crashlytics.setCustomKey("GLSL Version"  , shading );
+       crashlytics.setCustomKey("GLversion"     , version );
+       crashlytics.setCustomKey("GL Vendor "    , vendor  );
+       crashlytics.setCustomKey("GLSLrenderer"  , renderer);
+       crashlytics.recordException(ex);
+       }
+
+     int glsl = DistortedLibrary.getGLSL();
+
+     if( glsl< 300 && !mErrorShown )
+       {
+       mErrorShown = true;
+       PatternActivity act = (PatternActivity)mView.getContext();
+       act.OpenGLError();
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public InputStream localFile(int fileID)
+     {
+     return mResources.openRawResource(fileID);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void logMessage(String message)
+     {
+     android.util.Log.e("Rubik", message );
+     }
+}
diff --git a/src/main/java/org/distorted/patterns/PatternSurfaceView.java b/src/main/java/org/distorted/patterns/PatternSurfaceView.java
new file mode 100644
index 00000000..0c9aab7a
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/PatternSurfaceView.java
@@ -0,0 +1,149 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObjectNode;
+import org.distorted.os.OSInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PatternSurfaceView extends GLSurfaceView
+{
+    private ObjectControl mObjectController;
+    private OSInterface mInterface;
+    private PatternRenderer mRenderer;
+    private boolean mCreated;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)));
+
+      if( !mCreated )
+        {
+        mCreated = true;
+        mObjectController.createNode(width,height);
+        TwistyObjectNode objectNode = mObjectController.getNode();
+        objectNode.glDepthMask(false);
+        objectNode.glStencilMask(0);
+        mRenderer.getScreen().attach(objectNode);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    PatternRenderer getRenderer()
+      {
+      return mRenderer;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    OSInterface getInterface()
+      {
+      return mInterface;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    ObjectControl getObjectControl()
+      {
+      return mObjectController;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public PatternSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      mCreated = false;
+
+      if(!isInEditMode())
+        {
+        PatternActivity act = (PatternActivity)context;
+        PatternObjectLibInterface ref = new PatternObjectLibInterface(act);
+        mInterface = new OSInterface(act,ref);
+        mObjectController = new ObjectControl(mInterface);
+        mRenderer = new PatternRenderer(this);
+
+        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        try
+          {
+          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+          int esVersion = configurationInfo.reqGlEsVersion>>16;
+          setEGLContextClientVersion(esVersion);
+          setRenderer(mRenderer);
+          }
+        catch(Exception ex)
+          {
+          act.OpenGLError();
+
+          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+          String version = GLES30.glGetString(GLES30.GL_VERSION);
+          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("GLSL Version"  , shading );
+          crashlytics.setCustomKey("GL version"    , version );
+          crashlytics.setCustomKey("GL Vendor "    , vendor  );
+          crashlytics.setCustomKey("GLSL renderer" , renderer);
+          crashlytics.recordException(ex);
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onPause()
+      {
+      super.onPause();
+      mObjectController.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onResume()
+      {
+      super.onResume();
+      mObjectController.onResume();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @SuppressLint("ClickableViewAccessibility")
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      mInterface.setMotionEvent(event);
+      int mode = ScreenList.getMode();
+      return mObjectController.onTouchEvent(mode);
+      }
+}
+
diff --git a/src/main/java/org/distorted/patterns/ScreenAbstract.java b/src/main/java/org/distorted/patterns/ScreenAbstract.java
new file mode 100644
index 00000000..26dc7374
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/ScreenAbstract.java
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class ScreenAbstract
+  {
+  abstract void enterScreen(PatternActivity act);
+  abstract void leaveScreen(PatternActivity act);
+  public abstract void savePreferences(SharedPreferences.Editor editor);
+  public abstract void restorePreferences(SharedPreferences preferences);
+  }
diff --git a/src/main/java/org/distorted/patterns/ScreenList.java b/src/main/java/org/distorted/patterns/ScreenList.java
new file mode 100644
index 00000000..f962cc71
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/ScreenList.java
@@ -0,0 +1,148 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum ScreenList
+  {
+  PATT ( null, MODE_DRAG, new ScreenPattern()  ),
+  ;
+
+  public static final int LENGTH = values().length;
+  private static final ScreenList[] screens;
+  private final ScreenList mBack;
+  private final int mMode;
+  private final ScreenAbstract mClass;
+
+  private static ScreenList mCurrScreen;
+
+  static
+    {
+    int i = 0;
+    screens = new ScreenList[LENGTH];
+    for(ScreenList state: ScreenList.values()) screens[i++] = state;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreen(int ordinal)
+    {
+    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : PATT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreenFromName(String name)
+    {
+    for(int i=0; i<LENGTH; i++)
+      {
+      if( name.equals(screens[i].name()) )
+        {
+        return screens[i];
+        }
+      }
+
+    return PATT;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getCurrentScreen()
+    {
+    return mCurrScreen;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getMode()
+    {
+    return mCurrScreen.mMode;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void savePreferences(SharedPreferences.Editor editor)
+    {
+    editor.putString("curr_state_name", mCurrScreen.name() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void restorePreferences(SharedPreferences preferences)
+    {
+    String currScreenName = preferences.getString("curr_state_name", ScreenList.PATT.name() );
+    mCurrScreen = getScreenFromName(currScreenName);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void goBack(PatternActivity act)
+    {
+    switchScreen(act, mCurrScreen.mBack );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void setScreen(PatternActivity act)
+    {
+    mCurrScreen.enterScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void switchScreen(PatternActivity act, ScreenList next)
+    {
+    if( next!=null )
+      {
+      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
+      next.enterScreen(act);
+      mCurrScreen = next;
+      }
+    else
+      {
+      act.finish();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
+    {
+    mBack = back;
+    mMode = mode;
+    mClass= clazz;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScreenAbstract getScreenClass()
+    {
+    return mClass;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void leaveScreen(PatternActivity act)
+    {
+    mClass.leaveScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enterScreen(PatternActivity act)
+    {
+    mClass.enterScreen(act);
+    }
+  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/patterns/ScreenPattern.java b/src/main/java/org/distorted/patterns/ScreenPattern.java
new file mode 100644
index 00000000..74a4a9b0
--- /dev/null
+++ b/src/main/java/org/distorted/patterns/ScreenPattern.java
@@ -0,0 +1,366 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.patterns;
+
+import android.content.SharedPreferences;
+import android.os.Bundle;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.dialogs.DialogPattern;
+import org.distorted.helpers.TransparentButton;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.patterns.RubikPattern;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenPattern extends ScreenAbstract
+  {
+  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
+  private TransparentImageButton mPrevPatt, mNextPatt;
+  private TransparentButton mTextButton;
+  private TextView mMovesText;
+  private int mNumMoves;
+  private int mPatternOrdinal, mCategory, mPattern;
+  private float mButtonSize;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenPattern()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PatternActivity act)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PatternActivity act)
+    {
+    float width = act.getScreenWidthInPixels();
+    mButtonSize = width*PatternActivity.BUTTON_TEXT_SIZE;
+
+    mPatternOrdinal = -1;
+    mCategory       = -1;
+    mPattern        = -1;
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    setupPrevPatt(act);
+    setupNextPatt(act);
+    setupTextButton(act,width);
+
+    LinearLayout.LayoutParams pTL = new LinearLayout.LayoutParams((int)(  width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams pTM = new LinearLayout.LayoutParams((int)(2*width/3),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams pTR = new LinearLayout.LayoutParams((int)(  width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutTL = new LinearLayout(act);
+    layoutTL.setLayoutParams(pTL);
+    LinearLayout layoutTM = new LinearLayout(act);
+    layoutTM.setLayoutParams(pTM);
+    LinearLayout layoutTR = new LinearLayout(act);
+    layoutTR.setLayoutParams(pTR);
+
+    layoutTL.addView(mPrevPatt);
+    layoutTM.addView(mTextButton);
+    layoutTR.addView(mNextPatt);
+
+    layoutTop.addView(layoutTL);
+    layoutTop.addView(layoutTM);
+    layoutTop.addView(layoutTR);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width);
+
+    LinearLayout.LayoutParams pBL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams pBM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams pBR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutBL = new LinearLayout(act);
+    layoutBL.setLayoutParams(pBL);
+    LinearLayout layoutBM = new LinearLayout(act);
+    layoutBM.setLayoutParams(pBM);
+    LinearLayout layoutBR = new LinearLayout(act);
+    layoutBR.setLayoutParams(pBR);
+
+    layoutBL.addView(mPrevButton);
+    layoutBL.addView(mMovesText);
+    layoutBL.addView(mNextButton);
+
+    setupBackButton(act);
+
+    layoutBR.addView(mBackButton);
+
+    layoutBot.addView(layoutBL);
+    layoutBot.addView(layoutBM);
+    layoutBot.addView(layoutBR);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void showDialog(final PatternActivity act)
+    {
+    int ordinal = act.getObjectOrdinal();
+    Bundle bundle = new Bundle();
+    bundle.putString("argument", String.valueOf(ordinal) );
+    DialogPattern diag = new DialogPattern();
+    diag.setArguments(bundle);
+    diag.show( act.getSupportFragmentManager(), DialogPattern.getDialogTag());
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevPatt(final PatternActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevPatt = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevPatt.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( mPatternOrdinal<0 ) showDialog(act);
+        else prevPattern(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextPatt(final PatternActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextPatt = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextPatt.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( mPatternOrdinal<0 ) showDialog(act);
+        else nextPattern(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextButton(final PatternActivity act, float width)
+    {
+    float textSize = width*PatternActivity.TITLE_TEXT_SIZE;
+    mTextButton = new TransparentButton(act,R.string.choose_pattern,textSize);
+
+    mTextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        showDialog(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PatternActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final PatternActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( mPatternOrdinal>=0 )
+          {
+          RubikPattern pattern = RubikPattern.getInstance();
+          ObjectControl control = act.getControl();
+          pattern.backMove(control, mPatternOrdinal, mCategory, mPattern);
+          int currMove = pattern.getCurMove(mPatternOrdinal, mCategory, mPattern);
+          mMovesText.setText(act.getString(R.string.mo_placeholder, currMove, mNumMoves));
+          }
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final PatternActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( mPatternOrdinal>=0 )
+          {
+          RubikPattern pattern = RubikPattern.getInstance();
+          ObjectControl control = act.getControl();
+          pattern.makeMove(control, mPatternOrdinal, mCategory, mPattern);
+          int currMove = pattern.getCurMove(mPatternOrdinal, mCategory, mPattern);
+          mMovesText.setText(act.getString(R.string.mo_placeholder, currMove, mNumMoves));
+          }
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final PatternActivity act, final float width)
+    {
+    int padding = (int)(width*PatternActivity.PADDING);
+    int margin  = (int)(width*PatternActivity.SMALL_MARGIN);
+
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesText = new TextView(act);
+    mMovesText.setTextSize(20);
+    mMovesText.setLayoutParams(params);
+    mMovesText.setPadding(padding,0,padding,0);
+    mMovesText.setGravity(Gravity.CENTER);
+    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesText.setText(act.getString(R.string.mo_placeholder,0,0));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setPattern(final PatternActivity act, int ordinal, int category, int pattern)
+    {
+    mPatternOrdinal = ordinal;
+    mCategory       = category;
+    mPattern        = pattern;
+
+    RubikPattern patt = RubikPattern.getInstance();
+    String patternName = patt.getPatternName(ordinal,category,pattern);
+    mTextButton.setText(patternName);
+
+    mNumMoves   = patt.getNumMoves(ordinal,category,pattern);
+    int currMove= patt.getCurMove(ordinal,category,pattern);
+
+    mMovesText.setText(act.getString(R.string.mo_placeholder,currMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void prevPattern(final PatternActivity act)
+    {
+    RubikPattern patt = RubikPattern.getInstance();
+
+    if( mPattern>0 )
+      {
+      setPattern(act,mPatternOrdinal,mCategory,mPattern-1);
+      }
+    else
+      {
+      if( mCategory>0 )
+        {
+        int numPat = patt.getNumPatterns(mPatternOrdinal,mCategory-1);
+        setPattern(act,mPatternOrdinal,mCategory-1,numPat-1);
+        }
+      else
+        {
+        int numCat = patt.getNumCategories(mPatternOrdinal);
+        int numPat = patt.getNumPatterns(mPatternOrdinal,numCat-1);
+        setPattern(act,mPatternOrdinal,numCat-1,numPat-1);
+        }
+      }
+
+    int[][] moves = patt.reInitialize(mPatternOrdinal, mCategory, mPattern);
+    ObjectControl control = act.getControl();
+    if( control!=null ) control.initializeObject(moves);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void nextPattern(final PatternActivity act)
+    {
+    RubikPattern patt = RubikPattern.getInstance();
+    int numPat = patt.getNumPatterns(mPatternOrdinal,mCategory);
+
+    if( mPattern<numPat-1 )
+      {
+      setPattern(act,mPatternOrdinal,mCategory,mPattern+1);
+      }
+    else
+      {
+      int numCat = patt.getNumCategories(mPatternOrdinal);
+
+      if( mCategory<numCat-1 )
+        {
+        setPattern(act,mPatternOrdinal,mCategory+1,0);
+        }
+      else
+        {
+        setPattern(act,mPatternOrdinal,0,0);
+        }
+      }
+
+    int[][] moves = patt.reInitialize(mPatternOrdinal, mCategory, mPattern);
+    ObjectControl control = act.getControl();
+    if( control!=null ) control.initializeObject(moves);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/patternui/PatternActivity.java b/src/main/java/org/distorted/patternui/PatternActivity.java
deleted file mode 100644
index 57e49943..00000000
--- a/src/main/java/org/distorted/patternui/PatternActivity.java
+++ /dev/null
@@ -1,204 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.DisplayCutout;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import org.distorted.dialogs.DialogError;
-import org.distorted.helpers.BaseActivity;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.library.main.DistortedScreen;
-import org.distorted.main.R;
-import org.distorted.objectlib.main.InitAssets;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
-import org.distorted.os.OSInterface;
-
-import java.io.InputStream;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PatternActivity extends BaseActivity
-{
-    public static final float RATIO_UPP       = 0.100f;
-    public static final float PADDING         = 0.010f;
-    public static final float SMALL_MARGIN    = 0.004f;
-    public static final float BUTTON_TEXT_SIZE= 0.050f;
-    public static final float TITLE_TEXT_SIZE = 0.045f;
-
-    private static final int ACTIVITY_NUMBER = 5;
-    private static final float RATIO_INSET= 0.09f;
-
-    private int mObjectOrdinal;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    protected void onCreate(Bundle savedState)
-      {
-      super.onCreate(savedState);
-      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
-      setContentView(R.layout.pattern);
-
-      Bundle b = getIntent().getExtras();
-      mObjectOrdinal = b!=null ? b.getInt("obj") : 0;
-
-      computeScreenDimensions();
-      hideNavigationBar();
-      cutoutHack();
-      computeLowerBarHeight(RATIO_BAR);
-      computeUpperBarHeight(RATIO_UPP);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onAttachedToWindow()
-      {
-      super.onAttachedToWindow();
-
-      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
-        {
-        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
-        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
-
-        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
-        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
-        paramsHid.height = (int)(insetHeight*RATIO_INSET);
-        layoutHid.setLayoutParams(paramsHid);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onPause() 
-      {
-      super.onPause();
-      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
-      view.onPause();
-      DistortedLibrary.onPause(ACTIVITY_NUMBER);
-      savePreferences();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onResume() 
-      {
-      super.onResume();
-      DistortedLibrary.onResume(ACTIVITY_NUMBER);
-      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
-      view.onResume();
-
-      createObject();
-      restorePreferences();
-      ScreenList.setScreen(this);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onDestroy() 
-      {
-      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
-      super.onDestroy();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void savePreferences()
-      {
-      SharedPreferences.Editor editor = mPreferences.edit();
-
-      for(int i=0; i< ScreenList.LENGTH; i++ )
-        ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
-
-      ScreenList.savePreferences(editor);
-
-      boolean success = editor.commit();
-      if( !success ) android.util.Log.e("D", "Failed to save preferences");
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void restorePreferences()
-      {
-      for (int i=0; i<ScreenList.LENGTH; i++)
-        ScreenList.getScreen(i).getScreenClass().restorePreferences(mPreferences);
-
-      ScreenList.restorePreferences(mPreferences);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void OpenGLError()
-      {
-      DialogError errDiag = new DialogError();
-      errDiag.show(getSupportFragmentManager(), null);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public TwistyObject getObject()
-      {
-      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
-      return view.getObjectControl().getObject();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public DistortedScreen getScreen()
-      {
-      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
-      return view.getRenderer().getScreen();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public ObjectControl getControl()
-      {
-      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
-      return view.getObjectControl();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getObjectOrdinal()
-      {
-      return mObjectOrdinal;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void createObject()
-      {
-      PatternSurfaceView view = findViewById(R.id.patternSurfaceView);
-      ObjectControl control = view.getObjectControl();
-      RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
-      int iconMode  = TwistyObject.MODE_NORM;
-      InputStream jsonStream = object==null ? null : object.getObjectStream(this);
-      InputStream meshStream = object==null ? null : object.getMeshStream(this);
-      String name = object==null ? "NULL" : object.getUpperName();
-      OSInterface os = view.getInterface();
-      InitAssets asset = new InitAssets(jsonStream,meshStream,os);
-
-      control.changeIfDifferent(mObjectOrdinal,name,iconMode,asset);
-      }
-}
diff --git a/src/main/java/org/distorted/patternui/PatternObjectLibInterface.java b/src/main/java/org/distorted/patternui/PatternObjectLibInterface.java
deleted file mode 100644
index 3dc746c0..00000000
--- a/src/main/java/org/distorted/patternui/PatternObjectLibInterface.java
+++ /dev/null
@@ -1,153 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.library.message.EffectMessageSender;
-import org.distorted.main.BuildConfig;
-import org.distorted.objectlib.helpers.BlockController;
-import org.distorted.objectlib.helpers.ObjectLibInterface;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PatternObjectLibInterface implements ObjectLibInterface
-{
-  PatternObjectLibInterface(PatternActivity act) { }
-  public void onScrambleEffectFinished() { }
-  public void onRemoveRotation(int axis, int row, int angle) { }
-  public void onBeginRotation() { }
-  public void failedToDrag() { }
-  public void onSolved() { }
-  public void onObjectCreated(long time) { }
-  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
-  public void onReplaceModeDown(int cubit, int face) { }
-  public void onReplaceModeUp() { }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportProblem(String problem, boolean recordException)
-    {
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.e("libInterface", problem);
-      }
-    else
-      {
-      if( recordException )
-        {
-        Exception ex = new Exception(problem);
-        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-        crashlytics.setCustomKey("problem" , problem);
-        crashlytics.recordException(ex);
-        }
-      else
-        {
-        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-        crashlytics.log(problem);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportScramblingProblem(int place, long pause, long resume, long time)
-    {
-    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause );
-      crashlytics.setCustomKey("resume", resume );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportRotationProblem(int place, long pause, long resume, long time)
-    {
-    String error = "ROTATION BLOCK "+place+" blocked for "+time;
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause );
-      crashlytics.setCustomKey("resume", resume);
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportThreadProblem(int place, long pause, long resume, long time)
-    {
-    String error = EffectMessageSender.reportState();
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause  );
-      crashlytics.setCustomKey("resume", resume );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
-    {
-    switch(type)
-      {
-      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
-      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
-      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportJSONError(String error, int ordinal)
-    {
-    RubikObject object = RubikObjectList.getObject(ordinal);
-    String name = object==null ? "NULL" : object.getUpperName();
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("name" , name );
-      crashlytics.setCustomKey("JSONerror", error );
-      crashlytics.recordException(ex);
-      }
-    }
-}
diff --git a/src/main/java/org/distorted/patternui/PatternRenderer.java b/src/main/java/org/distorted/patternui/PatternRenderer.java
deleted file mode 100644
index 64902ef4..00000000
--- a/src/main/java/org/distorted/patternui/PatternRenderer.java
+++ /dev/null
@@ -1,159 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import android.content.res.Resources;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.VertexEffectQuaternion;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.library.main.DistortedScreen;
-import org.distorted.library.main.InternalOutputSurface;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.main.BuildConfig;
-import org.distorted.objectlib.effects.BaseEffect;
-import org.distorted.objectlib.main.ObjectControl;
-
-import java.io.InputStream;
-
-import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.opengles.GL10;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PatternRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
-{
-   private final PatternSurfaceView mView;
-   private final Resources mResources;
-   private final DistortedScreen mScreen;
-   private final ObjectControl mControl;
-   private boolean mErrorShown;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   PatternRenderer(PatternSurfaceView v)
-     {
-     mView = v;
-     mResources = v.getResources();
-
-     mErrorShown = false;
-     mControl = v.getObjectControl();
-     mScreen = new DistortedScreen();
-
-     PatternActivity act = (PatternActivity)v.getContext();
-     act.setUpBackgroundColor(mScreen);
-
-     mScreen.enableDepthStencil(InternalOutputSurface.DEPTH_NO_STENCIL);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   DistortedScreen getScreen()
-     {
-     return mScreen;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// various things are done here delayed, 'after the next render' as not to be done mid-render and
-// cause artifacts.
-
-   @Override
-   public void onDrawFrame(GL10 glUnused)
-     {
-     long time = System.currentTimeMillis();
-     mControl.preRender();
-     mScreen.render(time);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceChanged(GL10 glUnused, int width, int height)
-      {
-      mScreen.resize(width,height);
-      mView.setScreenSize(width,height);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
-      {
-      DistortedLibrary.setMax(EffectType.VERTEX,ObjectControl.MAX_QUATS+1);
-      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
-
-      VertexEffectRotate.enable();
-      VertexEffectQuaternion.enable();
-      BaseEffect.Type.enableEffects();
-
-      DistortedLibrary.onSurfaceCreated(this,1);
-      DistortedLibrary.setCull(true);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void distortedException(Exception ex)
-     {
-     String message = ex.getMessage();
-     String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
-     String version = GLES30.glGetString(GLES30.GL_VERSION);
-     String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
-     String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
-
-     if( message==null ) message = "exception NULL";
-
-     if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("DISTORTED", message );
-       android.util.Log.e("DISTORTED", "GLSL Version "+shading);
-       android.util.Log.e("DISTORTED", "GL Version "  +version);
-       android.util.Log.e("DISTORTED", "GL Vendor "   +vendor);
-       android.util.Log.e("DISTORTED", "GL Renderer " +renderer);
-       }
-     else
-       {
-       FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-       crashlytics.setCustomKey("DistortedError", message );
-       crashlytics.setCustomKey("GLSL Version"  , shading );
-       crashlytics.setCustomKey("GLversion"     , version );
-       crashlytics.setCustomKey("GL Vendor "    , vendor  );
-       crashlytics.setCustomKey("GLSLrenderer"  , renderer);
-       crashlytics.recordException(ex);
-       }
-
-     int glsl = DistortedLibrary.getGLSL();
-
-     if( glsl< 300 && !mErrorShown )
-       {
-       mErrorShown = true;
-       PatternActivity act = (PatternActivity)mView.getContext();
-       act.OpenGLError();
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public InputStream localFile(int fileID)
-     {
-     return mResources.openRawResource(fileID);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void logMessage(String message)
-     {
-     android.util.Log.e("Rubik", message );
-     }
-}
diff --git a/src/main/java/org/distorted/patternui/PatternSurfaceView.java b/src/main/java/org/distorted/patternui/PatternSurfaceView.java
deleted file mode 100644
index 18a5559b..00000000
--- a/src/main/java/org/distorted/patternui/PatternSurfaceView.java
+++ /dev/null
@@ -1,149 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ConfigurationInfo;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObjectNode;
-import org.distorted.os.OSInterface;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PatternSurfaceView extends GLSurfaceView
-{
-    private ObjectControl mObjectController;
-    private OSInterface mInterface;
-    private PatternRenderer mRenderer;
-    private boolean mCreated;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setScreenSize(int width, int height)
-      {
-      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)));
-
-      if( !mCreated )
-        {
-        mCreated = true;
-        mObjectController.createNode(width,height);
-        TwistyObjectNode objectNode = mObjectController.getNode();
-        objectNode.glDepthMask(false);
-        objectNode.glStencilMask(0);
-        mRenderer.getScreen().attach(objectNode);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    PatternRenderer getRenderer()
-      {
-      return mRenderer;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    OSInterface getInterface()
-      {
-      return mInterface;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    ObjectControl getObjectControl()
-      {
-      return mObjectController;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public PatternSurfaceView(Context context, AttributeSet attrs)
-      {
-      super(context,attrs);
-
-      mCreated = false;
-
-      if(!isInEditMode())
-        {
-        PatternActivity act = (PatternActivity)context;
-        PatternObjectLibInterface ref = new PatternObjectLibInterface(act);
-        mInterface = new OSInterface(act,ref);
-        mObjectController = new ObjectControl(mInterface);
-        mRenderer = new PatternRenderer(this);
-
-        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
-        try
-          {
-          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-          int esVersion = configurationInfo.reqGlEsVersion>>16;
-          setEGLContextClientVersion(esVersion);
-          setRenderer(mRenderer);
-          }
-        catch(Exception ex)
-          {
-          act.OpenGLError();
-
-          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
-          String version = GLES30.glGetString(GLES30.GL_VERSION);
-          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
-          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
-
-          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-          crashlytics.setCustomKey("GLSL Version"  , shading );
-          crashlytics.setCustomKey("GL version"    , version );
-          crashlytics.setCustomKey("GL Vendor "    , vendor  );
-          crashlytics.setCustomKey("GLSL renderer" , renderer);
-          crashlytics.recordException(ex);
-          }
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onPause()
-      {
-      super.onPause();
-      mObjectController.onPause();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onResume()
-      {
-      super.onResume();
-      mObjectController.onResume();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @SuppressLint("ClickableViewAccessibility")
-    @Override
-    public boolean onTouchEvent(MotionEvent event)
-      {
-      mInterface.setMotionEvent(event);
-      int mode = ScreenList.getMode();
-      return mObjectController.onTouchEvent(mode);
-      }
-}
-
diff --git a/src/main/java/org/distorted/patternui/ScreenAbstract.java b/src/main/java/org/distorted/patternui/ScreenAbstract.java
deleted file mode 100644
index e2d4676d..00000000
--- a/src/main/java/org/distorted/patternui/ScreenAbstract.java
+++ /dev/null
@@ -1,22 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import android.content.SharedPreferences;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class ScreenAbstract
-  {
-  abstract void enterScreen(PatternActivity act);
-  abstract void leaveScreen(PatternActivity act);
-  public abstract void savePreferences(SharedPreferences.Editor editor);
-  public abstract void restorePreferences(SharedPreferences preferences);
-  }
diff --git a/src/main/java/org/distorted/patternui/ScreenList.java b/src/main/java/org/distorted/patternui/ScreenList.java
deleted file mode 100644
index fbf62cb9..00000000
--- a/src/main/java/org/distorted/patternui/ScreenList.java
+++ /dev/null
@@ -1,148 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
-
-import android.content.SharedPreferences;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public enum ScreenList
-  {
-  PATT ( null, MODE_DRAG, new ScreenPattern()  ),
-  ;
-
-  public static final int LENGTH = values().length;
-  private static final ScreenList[] screens;
-  private final ScreenList mBack;
-  private final int mMode;
-  private final ScreenAbstract mClass;
-
-  private static ScreenList mCurrScreen;
-
-  static
-    {
-    int i = 0;
-    screens = new ScreenList[LENGTH];
-    for(ScreenList state: ScreenList.values()) screens[i++] = state;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getScreen(int ordinal)
-    {
-    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : PATT;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getScreenFromName(String name)
-    {
-    for(int i=0; i<LENGTH; i++)
-      {
-      if( name.equals(screens[i].name()) )
-        {
-        return screens[i];
-        }
-      }
-
-    return PATT;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getCurrentScreen()
-    {
-    return mCurrScreen;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getMode()
-    {
-    return mCurrScreen.mMode;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void savePreferences(SharedPreferences.Editor editor)
-    {
-    editor.putString("curr_state_name", mCurrScreen.name() );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void restorePreferences(SharedPreferences preferences)
-    {
-    String currScreenName = preferences.getString("curr_state_name", ScreenList.PATT.name() );
-    mCurrScreen = getScreenFromName(currScreenName);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void goBack(PatternActivity act)
-    {
-    switchScreen(act, mCurrScreen.mBack );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void setScreen(PatternActivity act)
-    {
-    mCurrScreen.enterScreen(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void switchScreen(PatternActivity act, ScreenList next)
-    {
-    if( next!=null )
-      {
-      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
-      next.enterScreen(act);
-      mCurrScreen = next;
-      }
-    else
-      {
-      act.finish();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
-    {
-    mBack = back;
-    mMode = mode;
-    mClass= clazz;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ScreenAbstract getScreenClass()
-    {
-    return mClass;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void leaveScreen(PatternActivity act)
-    {
-    mClass.leaveScreen(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void enterScreen(PatternActivity act)
-    {
-    mClass.enterScreen(act);
-    }
-  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/patternui/ScreenPattern.java b/src/main/java/org/distorted/patternui/ScreenPattern.java
deleted file mode 100644
index 74ca1060..00000000
--- a/src/main/java/org/distorted/patternui/ScreenPattern.java
+++ /dev/null
@@ -1,366 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.patternui;
-
-import android.content.SharedPreferences;
-import android.os.Bundle;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.dialogs.DialogPattern;
-import org.distorted.helpers.TransparentButton;
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.patterns.RubikPattern;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenPattern extends ScreenAbstract
-  {
-  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
-  private TransparentImageButton mPrevPatt, mNextPatt;
-  private TransparentButton mTextButton;
-  private TextView mMovesText;
-  private int mNumMoves;
-  private int mPatternOrdinal, mCategory, mPattern;
-  private float mButtonSize;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  ScreenPattern()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(PatternActivity act)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final PatternActivity act)
-    {
-    float width = act.getScreenWidthInPixels();
-    mButtonSize = width*PatternActivity.BUTTON_TEXT_SIZE;
-
-    mPatternOrdinal = -1;
-    mCategory       = -1;
-    mPattern        = -1;
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    setupPrevPatt(act);
-    setupNextPatt(act);
-    setupTextButton(act,width);
-
-    LinearLayout.LayoutParams pTL = new LinearLayout.LayoutParams((int)(  width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams pTM = new LinearLayout.LayoutParams((int)(2*width/3),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams pTR = new LinearLayout.LayoutParams((int)(  width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-
-    LinearLayout layoutTL = new LinearLayout(act);
-    layoutTL.setLayoutParams(pTL);
-    LinearLayout layoutTM = new LinearLayout(act);
-    layoutTM.setLayoutParams(pTM);
-    LinearLayout layoutTR = new LinearLayout(act);
-    layoutTR.setLayoutParams(pTR);
-
-    layoutTL.addView(mPrevPatt);
-    layoutTM.addView(mTextButton);
-    layoutTR.addView(mNextPatt);
-
-    layoutTop.addView(layoutTL);
-    layoutTop.addView(layoutTM);
-    layoutTop.addView(layoutTR);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    setupPrevButton(act);
-    setupNextButton(act);
-    setupTextView(act,width);
-
-    LinearLayout.LayoutParams pBL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams pBM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams pBR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
-
-    LinearLayout layoutBL = new LinearLayout(act);
-    layoutBL.setLayoutParams(pBL);
-    LinearLayout layoutBM = new LinearLayout(act);
-    layoutBM.setLayoutParams(pBM);
-    LinearLayout layoutBR = new LinearLayout(act);
-    layoutBR.setLayoutParams(pBR);
-
-    layoutBL.addView(mPrevButton);
-    layoutBL.addView(mMovesText);
-    layoutBL.addView(mNextButton);
-
-    setupBackButton(act);
-
-    layoutBR.addView(mBackButton);
-
-    layoutBot.addView(layoutBL);
-    layoutBot.addView(layoutBM);
-    layoutBot.addView(layoutBR);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void showDialog(final PatternActivity act)
-    {
-    int ordinal = act.getObjectOrdinal();
-    Bundle bundle = new Bundle();
-    bundle.putString("argument", String.valueOf(ordinal) );
-    DialogPattern diag = new DialogPattern();
-    diag.setArguments(bundle);
-    diag.show( act.getSupportFragmentManager(), DialogPattern.getDialogTag());
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevPatt(final PatternActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevPatt = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevPatt.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( mPatternOrdinal<0 ) showDialog(act);
-        else prevPattern(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextPatt(final PatternActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextPatt = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextPatt.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( mPatternOrdinal<0 ) showDialog(act);
-        else nextPattern(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextButton(final PatternActivity act, float width)
-    {
-    float textSize = width*PatternActivity.TITLE_TEXT_SIZE;
-    mTextButton = new TransparentButton(act,R.string.choose_pattern,textSize);
-
-    mTextButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        showDialog(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final PatternActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final PatternActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( mPatternOrdinal>=0 )
-          {
-          RubikPattern pattern = RubikPattern.getInstance();
-          ObjectControl control = act.getControl();
-          pattern.backMove(control, mPatternOrdinal, mCategory, mPattern);
-          int currMove = pattern.getCurMove(mPatternOrdinal, mCategory, mPattern);
-          mMovesText.setText(act.getString(R.string.mo_placeholder, currMove, mNumMoves));
-          }
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextButton(final PatternActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( mPatternOrdinal>=0 )
-          {
-          RubikPattern pattern = RubikPattern.getInstance();
-          ObjectControl control = act.getControl();
-          pattern.makeMove(control, mPatternOrdinal, mCategory, mPattern);
-          int currMove = pattern.getCurMove(mPatternOrdinal, mCategory, mPattern);
-          mMovesText.setText(act.getString(R.string.mo_placeholder, currMove, mNumMoves));
-          }
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextView(final PatternActivity act, final float width)
-    {
-    int padding = (int)(width*PatternActivity.PADDING);
-    int margin  = (int)(width*PatternActivity.SMALL_MARGIN);
-
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesText = new TextView(act);
-    mMovesText.setTextSize(20);
-    mMovesText.setLayoutParams(params);
-    mMovesText.setPadding(padding,0,padding,0);
-    mMovesText.setGravity(Gravity.CENTER);
-    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesText.setText(act.getString(R.string.mo_placeholder,0,0));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setPattern(final PatternActivity act, int ordinal, int category, int pattern)
-    {
-    mPatternOrdinal = ordinal;
-    mCategory       = category;
-    mPattern        = pattern;
-
-    RubikPattern patt = RubikPattern.getInstance();
-    String patternName = patt.getPatternName(ordinal,category,pattern);
-    mTextButton.setText(patternName);
-
-    mNumMoves   = patt.getNumMoves(ordinal,category,pattern);
-    int currMove= patt.getCurMove(ordinal,category,pattern);
-
-    mMovesText.setText(act.getString(R.string.mo_placeholder,currMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void prevPattern(final PatternActivity act)
-    {
-    RubikPattern patt = RubikPattern.getInstance();
-
-    if( mPattern>0 )
-      {
-      setPattern(act,mPatternOrdinal,mCategory,mPattern-1);
-      }
-    else
-      {
-      if( mCategory>0 )
-        {
-        int numPat = patt.getNumPatterns(mPatternOrdinal,mCategory-1);
-        setPattern(act,mPatternOrdinal,mCategory-1,numPat-1);
-        }
-      else
-        {
-        int numCat = patt.getNumCategories(mPatternOrdinal);
-        int numPat = patt.getNumPatterns(mPatternOrdinal,numCat-1);
-        setPattern(act,mPatternOrdinal,numCat-1,numPat-1);
-        }
-      }
-
-    int[][] moves = patt.reInitialize(mPatternOrdinal, mCategory, mPattern);
-    ObjectControl control = act.getControl();
-    if( control!=null ) control.initializeObject(moves);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void nextPattern(final PatternActivity act)
-    {
-    RubikPattern patt = RubikPattern.getInstance();
-    int numPat = patt.getNumPatterns(mPatternOrdinal,mCategory);
-
-    if( mPattern<numPat-1 )
-      {
-      setPattern(act,mPatternOrdinal,mCategory,mPattern+1);
-      }
-    else
-      {
-      int numCat = patt.getNumCategories(mPatternOrdinal);
-
-      if( mCategory<numCat-1 )
-        {
-        setPattern(act,mPatternOrdinal,mCategory+1,0);
-        }
-      else
-        {
-        setPattern(act,mPatternOrdinal,0,0);
-        }
-      }
-
-    int[][] moves = patt.reInitialize(mPatternOrdinal, mCategory, mPattern);
-    ObjectControl control = act.getControl();
-    if( control!=null ) control.initializeObject(moves);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-
-    }
-  }
diff --git a/src/main/java/org/distorted/play/PlayActivity.java b/src/main/java/org/distorted/play/PlayActivity.java
new file mode 100644
index 00000000..465b8a64
--- /dev/null
+++ b/src/main/java/org/distorted/play/PlayActivity.java
@@ -0,0 +1,313 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2022 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import java.io.InputStream;
+
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.DisplayCutout;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import com.google.firebase.analytics.FirebaseAnalytics;
+
+import org.distorted.dialogs.DialogScores;
+import org.distorted.helpers.RubikScores;
+import org.distorted.helpers.BaseActivity;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.objectlib.main.InitAssets;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.dialogs.DialogError;
+import org.distorted.helpers.RubikFiles;
+import org.distorted.main.R;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.os.OSInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PlayActivity extends BaseActivity implements DialogScores.ScoresInvoker
+{
+    public static final float TITLE_TEXT_SIZE= 0.060f;
+    private static final int ACTIVITY_NUMBER = 6;
+    private static final float RATIO_INSET   = 0.09f;
+
+    private static final String KEY_FREE = "movesController_free";
+    private static final String KEY_SOLV = "movesController_solv";
+
+    private String mObjectName;
+    private int mNumScrambles;
+    private boolean mObjectLocal;
+    private int mObjectOrdinal;
+    private int mLevel;
+    private boolean mModeFree;
+    private boolean mJustStarted;
+    private FirebaseAnalytics mFirebaseAnalytics;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle savedState)
+      {
+      super.onCreate(savedState);
+      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
+      setContentView(R.layout.play);
+
+      mJustStarted = true;
+      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
+
+      Bundle b = getIntent().getExtras();
+
+      if( b!=null )
+        {
+        mLevel         = b.getInt("level");
+        mObjectName    = b.getString("name");
+        mNumScrambles  = b.getInt("scrambles");
+        mObjectLocal   = b.getBoolean("local");
+        mObjectOrdinal = b.getInt("ordinal");
+        }
+      else
+        {
+        mLevel = -1;
+        mObjectName = "";
+        mNumScrambles = 0;
+        mObjectLocal = true;
+        mObjectOrdinal = 0;
+        }
+
+      mModeFree = (mLevel<0);
+
+      computeScreenDimensions();
+      hideNavigationBar();
+      cutoutHack();
+      computeUpperBarHeight(RATIO_BAR);
+      computeLowerBarHeight(RATIO_BAR);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onAttachedToWindow()
+      {
+      super.onAttachedToWindow();
+
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+        {
+        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
+        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
+
+        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
+        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
+        paramsHid.height = (int)(insetHeight*RATIO_INSET);
+        layoutHid.setLayoutParams(paramsHid);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      super.onPause();
+      PlayView view = findViewById(R.id.playView);
+      view.onPause();
+      savePreferences();
+      DistortedLibrary.onPause(ACTIVITY_NUMBER);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      DistortedLibrary.onResume(ACTIVITY_NUMBER);
+      PlayView view = findViewById(R.id.playView);
+      ObjectControl control = view.getObjectControl();
+      view.onResume();
+
+      restorePreferences();
+
+      ScreenList sl =  mJustStarted ?
+                      (mModeFree ? ScreenList.FREE : ScreenList.SCRA) :
+                      ScreenList.getCurrentScreen();
+
+      ScreenList.switchScreen(this,sl);
+
+      if( !mJustStarted ) restoreMoves();
+
+      if( mObjectName.length()>0 )
+        {
+        changeIfDifferent(mObjectName,mObjectLocal,mObjectOrdinal,control);
+        }
+
+      if( mJustStarted && !mModeFree )
+        {
+        control.scrambleObject(mNumScrambles);
+        }
+
+      mJustStarted = false;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      super.onDestroy();
+      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void savePreferences()
+    {
+    SharedPreferences.Editor editor = mPreferences.edit();
+
+    for( int i=0; i<ScreenList.LENGTH; i++ )
+      {
+      ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
+      }
+
+    ScreenList.savePreferences(editor);
+
+    ScreenList curr = ScreenList.getCurrentScreen();
+
+    if( curr==ScreenList.FREE )
+      {
+      ScreenFree free = (ScreenFree) ScreenList.FREE.getScreenClass();
+      free.saveMovePreferences(KEY_FREE,editor);
+      }
+    if( curr==ScreenList.SOLV )
+      {
+      ScreenSolving solv = (ScreenSolving) ScreenList.SOLV.getScreenClass();
+      solv.saveMovePreferences(KEY_SOLV,editor);
+      }
+
+    PlayView view = findViewById(R.id.playView);
+    OSInterface os = view.getInterface();
+    os.setEditor(editor);
+    view.getObjectControl().savePreferences();
+
+    RubikScores scores = RubikScores.getInstance();
+    scores.savePreferences(editor);
+
+    editor.apply();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void restorePreferences()
+    {
+    for( int i=0; i<ScreenList.LENGTH; i++)
+      {
+      ScreenList.getScreen(i).getScreenClass().restorePreferences(mPreferences);
+      }
+
+    if( !mJustStarted ) ScreenList.restorePreferences(mPreferences);
+
+    PlayView view = findViewById(R.id.playView);
+    OSInterface os = view.getInterface();
+    os.setPreferences(mPreferences);
+    view.getObjectControl().restorePreferences();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void restoreMoves()
+    {
+    ScreenList curr = ScreenList.getCurrentScreen();
+
+    if( curr==ScreenList.FREE )
+      {
+      ScreenFree free = (ScreenFree) ScreenList.FREE.getScreenClass();
+      free.restoreMovePreferences(this,KEY_FREE,mPreferences);
+      }
+    if( curr==ScreenList.SOLV )
+      {
+      ScreenSolving solv = (ScreenSolving) ScreenList.SOLV.getScreenClass();
+      solv.restoreMovePreferences(this,KEY_SOLV,mPreferences);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void OpenGLError()
+      {
+      DialogError errDiag = new DialogError();
+      errDiag.show(getSupportFragmentManager(), null);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void changeIfDifferent(String name, boolean local, int ordinal, ObjectControl control)
+      {
+      if( local )
+        {
+        RubikFiles files = RubikFiles.getInstance();
+        int iconMode = TwistyObject.MODE_NORM;
+        InputStream jsonStream = files.openFile(this, name+"_object.json");
+        InitAssets asset = new InitAssets(jsonStream, null, null);
+        control.changeIfDifferent(ordinal,name,iconMode,asset);
+        }
+      else
+        {
+        RubikObject object = RubikObjectList.getObject(ordinal);
+        int iconMode = TwistyObject.MODE_NORM;
+        InputStream jsonStream = object==null ? null : object.getObjectStream(this);
+        InputStream meshStream = object==null ? null : object.getMeshStream(this);
+        PlayView view = findViewById(R.id.playView);
+        OSInterface os = view.getInterface();
+        InitAssets asset = new InitAssets(jsonStream, meshStream, os);
+        control.changeIfDifferent(ordinal, name, iconMode, asset);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public FirebaseAnalytics getAnalytics()
+      {
+      return mFirebaseAnalytics;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getNumScrambles()
+      {
+      return mNumScrambles;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getLevel()
+      {
+      return mLevel;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getObjectOrdinal()
+      {
+      return mObjectOrdinal;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public ObjectControl getControl()
+      {
+      PlayView view = findViewById(R.id.playView);
+      return view.getObjectControl();
+      }
+}
diff --git a/src/main/java/org/distorted/play/PlayLibInterface.java b/src/main/java/org/distorted/play/PlayLibInterface.java
new file mode 100644
index 00000000..d7458df1
--- /dev/null
+++ b/src/main/java/org/distorted/play/PlayLibInterface.java
@@ -0,0 +1,454 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import static org.distorted.helpers.RubikScores.RECORD_FIRST;
+import static org.distorted.helpers.RubikScores.RECORD_NEW;
+import static org.distorted.helpers.RubikScores.RECORD_NOT_NEW;
+
+import android.os.Bundle;
+
+import androidx.annotation.NonNull;
+
+import java.lang.ref.WeakReference;
+
+import com.google.android.play.core.review.ReviewInfo;
+import com.google.android.play.core.review.ReviewManager;
+import com.google.android.play.core.review.ReviewManagerFactory;
+import com.google.android.gms.tasks.OnCompleteListener;
+import com.google.android.gms.tasks.OnFailureListener;
+import com.google.android.gms.tasks.Task;
+import com.google.firebase.analytics.FirebaseAnalytics;
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.dialogs.DialogNewRecord;
+import org.distorted.dialogs.DialogScoresView;
+import org.distorted.dialogs.DialogSolved;
+import org.distorted.helpers.RubikNetwork;
+import org.distorted.helpers.RubikScores;
+import org.distorted.library.message.EffectMessageSender;
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.ObjectLibInterface;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.main.BuildConfig;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PlayLibInterface implements ObjectLibInterface
+{
+  private final WeakReference<PlayActivity> mAct;
+  private int mIsNewRecord;
+  private int mNewRecord;
+  private boolean mReviewAsked;
+  private int mNumRotations, mNumScrambles;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  PlayLibInterface(PlayActivity act)
+    {
+    mAct = new WeakReference<>(act);
+    mReviewAsked = false;
+    mNumRotations = 0;
+    mNumScrambles = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onObjectCreated(long time) { }
+  public void onReplaceModeDown(int cubit, int face) { }
+  public void onReplaceModeUp() { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void analyticsReport(PlayActivity act, String message, String name, long timeBegin)
+    {
+    long elapsed = System.currentTimeMillis() - timeBegin;
+    String msg = message+" startTime: "+timeBegin+" elapsed: "+elapsed+" name: "+name;
+
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.d("libInterface", msg);
+      }
+    else
+      {
+      FirebaseAnalytics analytics = act.getAnalytics();
+
+      if( analytics!=null )
+        {
+        Bundle bundle = new Bundle();
+        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, msg);
+        analytics.logEvent(FirebaseAnalytics.Event.SHARE, bundle);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportRecord(PlayActivity act, long startTime, long endTime, String debug, int scrambleNum)
+    {
+    RubikScores scores  = RubikScores.getInstance();
+    int objectOrdinal = act.getObjectOrdinal();
+    String name = scores.getName();
+    RubikObject obj = RubikObjectList.getObject(objectOrdinal);
+    String objName = obj==null ? "NULL" : obj.getUpperName();
+
+    String record = objName+" time "+mNewRecord+" isNew: "+mIsNewRecord+" scrambleNum: "+scrambleNum;
+
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("libInterface", debug);
+      android.util.Log.e("libInterface", name);
+      android.util.Log.e("libInterface", record);
+      }
+    else
+      {
+      if( scrambleNum>=9 && mNewRecord<300*scrambleNum )
+        {
+        long timeNow = System.currentTimeMillis();
+        long elapsed = timeNow - startTime;
+        String suspicious ="start"+startTime+"end"+endTime+"elapsed"+elapsed+"obj"+objName+"record"+mNewRecord+"scrambles"+scrambleNum+debug;
+        RubikNetwork network = RubikNetwork.getInstance();
+        network.suspicious(suspicious,act);
+        }
+
+      FirebaseAnalytics analytics = act.getAnalytics();
+
+      if( analytics!=null )
+        {
+        Bundle bundle = new Bundle();
+        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, debug);
+        bundle.putString(FirebaseAnalytics.Param.CHARACTER, name);
+        bundle.putString(FirebaseAnalytics.Param.LEVEL, record);
+        analytics.logEvent(FirebaseAnalytics.Event.LEVEL_UP, bundle);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private Bundle createDialogBundle()
+    {
+    Bundle bundle = new Bundle();
+    String arg = DialogScoresView.formatRecord(mNewRecord);
+    bundle.putString("argument", arg );
+    return bundle;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void requestReview(PlayActivity act)
+    {
+    android.util.Log.e("D", "ASKING FOR REVIEW");
+
+    mReviewAsked = true;
+    final String name = RubikScores.getInstance().getName();
+    final long timeBegin = System.currentTimeMillis();
+    final ReviewManager manager = ReviewManagerFactory.create(act);
+    Task<ReviewInfo> request = manager.requestReviewFlow();
+
+    request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>()
+      {
+      @Override
+      public void onComplete (@NonNull Task<ReviewInfo> task)
+        {
+        if (task.isSuccessful())
+          {
+          ReviewInfo reviewInfo = task.getResult();
+          Task<Void> flow = manager.launchReviewFlow(act, reviewInfo);
+
+          flow.addOnFailureListener(new OnFailureListener()
+            {
+            @Override
+            public void onFailure(Exception e)
+            {
+            analyticsReport(act,"Failed", name, timeBegin);
+            }
+            });
+
+          flow.addOnCompleteListener(new OnCompleteListener<Void>()
+            {
+            @Override
+            public void onComplete(@NonNull Task<Void> task)
+              {
+              analyticsReport(act,"Complete", name, timeBegin);
+              }
+            });
+          }
+        else analyticsReport(act,"Not Successful", name, timeBegin);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onScrambleEffectFinished()
+    {
+    if( ScreenList.getCurrentScreen()==ScreenList.SCRA )
+      {
+      PlayActivity act = mAct.get();
+      RubikScores.getInstance().incrementNumPlays();
+
+      act.runOnUiThread(new Runnable()
+        {
+        @Override
+        public void run()
+          {
+          ScreenList.switchScreen(act,ScreenList.READ);
+          ObjectControl control = act.getControl();
+          control.unblockEverything();
+          }
+        });
+      }
+
+    mNumScrambles++;
+
+    if( mNumScrambles==10 && !mReviewAsked )
+      {
+      PlayActivity act = mAct.get();
+      requestReview(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onRemoveRotation(int axis, int row, int angle)
+    {
+    mNumRotations++;
+    PlayActivity act = mAct.get();
+
+    if( act!=null )
+      {
+      ScreenList screen = ScreenList.getCurrentScreen();
+
+      if( screen==ScreenList.FREE ||
+          screen==ScreenList.SOLV  ) ((ScreenBase)screen.getScreenClass()).addMove(act,axis,row,angle);
+      }
+
+    if( mNumRotations==40 && !mReviewAsked )
+      {
+      requestReview(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onBeginRotation()
+    {
+    if( ScreenList.getCurrentScreen()==ScreenList.READ )
+      {
+      ScreenSolving solving = (ScreenSolving) ScreenList.SOLV.getScreenClass();
+      solving.resetElapsed();
+      PlayActivity act = mAct.get();
+      ScreenList.switchScreen( act,ScreenList.SOLV );
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void failedToDrag()
+    {
+    PlayActivity act = mAct.get();
+
+    if( act!=null )
+      {
+      ScreenList screen = ScreenList.getCurrentScreen();
+
+      if( screen==ScreenList.FREE ||
+          screen==ScreenList.SOLV ||
+          screen==ScreenList.SCRA  ) ((ScreenBase)screen.getScreenClass()).reddenLock(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onSolved()
+    {
+    if( ScreenList.getCurrentScreen()==ScreenList.SOLV )
+      {
+      PlayActivity act = mAct.get();
+      ObjectControl control = act.getControl();
+      TwistyObject obj = control.getObject();
+      boolean submittable = obj.isSubmittable();
+      int objectOrdinal = act.getObjectOrdinal();
+      ScreenSolving solving = (ScreenSolving)ScreenList.SOLV.getScreenClass();
+      mNewRecord = solving.stopTimerAndGetRecord();
+      mIsNewRecord = submittable ? solving.setRecord(objectOrdinal) : RECORD_NOT_NEW;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum)
+    {
+    if( ScreenList.getCurrentScreen()==ScreenList.SOLV )
+      {
+      PlayActivity act = mAct.get();
+      ObjectControl control = act.getControl();
+      TwistyObject obj = control.getObject();
+      boolean submittable = obj.isSubmittable();
+
+      if( submittable ) reportRecord(act,startTime,endTime,debug,scrambleNum);
+
+      RubikScores scores = RubikScores.getInstance();
+      int numWins = scores.incrementNumWins();
+      int numRuns = scores.getNumRuns();
+
+      if( numRuns==3 || numRuns==6 || numWins==4 || numWins==20 || numWins==50 || numWins==80 || numWins==100)
+        {
+        requestReview(act);
+        }
+
+      switch(mIsNewRecord)
+        {
+        case RECORD_FIRST  :
+        case RECORD_NEW    : Bundle byes = createDialogBundle();
+                             DialogNewRecord dyes = new DialogNewRecord();
+                             dyes.setArguments(byes);
+                             dyes.show( act.getSupportFragmentManager(), DialogNewRecord.getDialogTag());
+                             break;
+        case RECORD_NOT_NEW: Bundle bno = createDialogBundle();
+                             DialogSolved dno = new DialogSolved();
+                             dno.setArguments(bno);
+                             dno.show( act.getSupportFragmentManager(), DialogSolved.getDialogTag());
+        break;
+        }
+
+      act.runOnUiThread(new Runnable()
+        {
+        @Override
+        public void run()
+          {
+          ScreenList.switchScreen( act,ScreenList.DONE );
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportProblem(String problem, boolean reportException)
+    {
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("interface", problem);
+      }
+    else
+      {
+      if( reportException )
+        {
+        Exception ex = new Exception(problem);
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.setCustomKey("problem" , problem);
+        crashlytics.recordException(ex);
+        }
+      else
+        {
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.log(problem);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportScramblingProblem(int place, long pause, long resume, long time)
+    {
+    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportRotationProblem(int place, long pause, long resume, long time)
+    {
+    String error = "ROTATION BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume);
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportThreadProblem(int place, long pause, long resume, long time)
+    {
+    String error = EffectMessageSender.reportState();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause  );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
+    {
+    switch(type)
+      {
+      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportJSONError(String error, int ordinal)
+    {
+    RubikObject object = RubikObjectList.getObject(ordinal);
+    String name = object==null ? "NULL" : object.getUpperName();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("name" , name );
+      crashlytics.setCustomKey("JSONerror", error );
+      crashlytics.recordException(ex);
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/play/PlayRenderer.java b/src/main/java/org/distorted/play/PlayRenderer.java
new file mode 100644
index 00000000..0ba574c2
--- /dev/null
+++ b/src/main/java/org/distorted/play/PlayRenderer.java
@@ -0,0 +1,111 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2022 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import java.io.InputStream;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+import android.content.res.Resources;
+import android.opengl.GLSurfaceView;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffectQuaternion;
+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.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectControl;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PlayRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
+{
+   private final PlayView mView;
+   private final Resources mResources;
+   private final DistortedScreen mScreen;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   PlayRenderer(PlayView v)
+     {
+     mView = v;
+     mResources = v.getResources();
+     mScreen = new DistortedScreen();
+
+     PlayActivity act = (PlayActivity)v.getContext();
+     act.setUpBackgroundColor(mScreen);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onDrawFrame(GL10 glUnused)
+     {
+     long time = System.currentTimeMillis();
+     mView.getObjectControl().preRender();
+     mScreen.render(time);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceChanged(GL10 glUnused, int width, int height)
+      {
+      mScreen.resize(width,height);
+      mView.setScreenSize(width,height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedScreen getScreen()
+     {
+     return mScreen;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
+      {
+      DistortedLibrary.setMax(EffectType.VERTEX, ObjectControl.MAX_QUATS+1);
+      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
+
+      VertexEffectRotate.enable();
+      VertexEffectQuaternion.enable();
+      BaseEffect.Type.enableEffects();
+
+      DistortedLibrary.onSurfaceCreated(this,1);
+      DistortedLibrary.setCull(true);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void distortedException(Exception ex)
+     {
+     android.util.Log.e("Play", "unexpected exception: "+ex.getMessage() );
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public InputStream localFile(int fileID)
+      {
+      return mResources.openRawResource(fileID);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void logMessage(String message)
+      {
+      android.util.Log.e("Play", message );
+      }
+}
diff --git a/src/main/java/org/distorted/play/PlayView.java b/src/main/java/org/distorted/play/PlayView.java
new file mode 100644
index 00000000..4b517acf
--- /dev/null
+++ b/src/main/java/org/distorted/play/PlayView.java
@@ -0,0 +1,143 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import static org.distorted.objectlib.main.ObjectControl.MODE_ROTATE;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObjectNode;
+import org.distorted.os.OSInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PlayView extends GLSurfaceView
+{
+    private ObjectControl mObjectController;
+    private OSInterface mInterface;
+    private PlayRenderer mRenderer;
+    private boolean mCreated;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)) );
+      mObjectController.setObjectScale(1.00f);
+
+      if( !mCreated )
+        {
+        mCreated = true;
+        mObjectController.createNode(width,height);
+        TwistyObjectNode objectNode = mObjectController.getNode();
+        mRenderer.getScreen().attach(objectNode);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    ObjectControl getObjectControl()
+      {
+      return mObjectController;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    OSInterface getInterface()
+      {
+      return mInterface;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public PlayView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      mCreated = false;
+
+      if(!isInEditMode())
+        {
+        PlayActivity act = (PlayActivity)context;
+        PlayLibInterface ref = new PlayLibInterface(act);
+        mInterface = new OSInterface(act,ref);
+        mObjectController = new ObjectControl(mInterface);
+        mObjectController.setRotateOnCreation(true);
+        mRenderer = new PlayRenderer(this);
+
+        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        try
+          {
+          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+          int esVersion = configurationInfo.reqGlEsVersion>>16;
+          setEGLContextClientVersion(esVersion);
+          setRenderer(mRenderer);
+          }
+        catch(Exception ex)
+          {
+          act.OpenGLError();
+
+          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+          String version = GLES30.glGetString(GLES30.GL_VERSION);
+          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("GLSL Version"  , shading );
+          crashlytics.setCustomKey("GL version"    , version );
+          crashlytics.setCustomKey("GL Vendor "    , vendor  );
+          crashlytics.setCustomKey("GLSL renderer" , renderer);
+          crashlytics.recordException(ex);
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onPause()
+      {
+      super.onPause();
+      mObjectController.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onResume()
+      {
+      super.onResume();
+      mObjectController.onResume();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @SuppressLint("ClickableViewAccessibility")
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      mInterface.setMotionEvent(event);
+      return mObjectController.onTouchEvent(MODE_ROTATE);
+      }
+}
+
diff --git a/src/main/java/org/distorted/play/ScreenAbstract.java b/src/main/java/org/distorted/play/ScreenAbstract.java
new file mode 100644
index 00000000..9cf3d49f
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenAbstract.java
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class ScreenAbstract
+  {
+  abstract void enterScreen(PlayActivity act);
+  abstract void leaveScreen(PlayActivity act);
+  public abstract void savePreferences(SharedPreferences.Editor editor);
+  public abstract void restorePreferences(SharedPreferences preferences);
+  }
diff --git a/src/main/java/org/distorted/play/ScreenBase.java b/src/main/java/org/distorted/play/ScreenBase.java
new file mode 100644
index 00000000..d0cc5d71
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenBase.java
@@ -0,0 +1,104 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+import android.widget.LinearLayout;
+
+import org.distorted.helpers.LockController;
+import org.distorted.helpers.MovesController;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.objectlib.main.ObjectControl;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+abstract class ScreenBase extends ScreenAbstract
+  {
+  private final LockController mLockController;
+  protected MovesController mMovesController;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void createBottomPane(final PlayActivity act, TransparentImageButton butt)
+    {
+    mMovesController.clearMoves(act);
+
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(params);
+    LinearLayout layoutMid = new LinearLayout(act);
+    layoutMid.setLayoutParams(params);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(params);
+
+    ObjectControl control = act.getControl();
+    mMovesController.setupButton(act,control);
+    layoutLeft.addView(mMovesController.getButton());
+    mLockController.setupButton(act,control);
+    layoutMid.addView(mLockController.getButton());
+
+    if( butt !=null ) layoutRight.addView(butt);
+
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutMid);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setLockState(final PlayActivity act)
+    {
+    boolean locked = act.getControl().retLocked();
+    mLockController.setState(act,locked);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public ScreenBase()
+    {
+    mLockController = new LockController();
+    mMovesController= new MovesController();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addMove(PlayActivity act, int axis, int row, int angle)
+    {
+    mMovesController.addMove(act,axis,row,angle);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reddenLock(final PlayActivity act)
+    {
+    ObjectControl control = act.getControl();
+    mLockController.reddenLock(act,control);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void saveMovePreferences(String key,SharedPreferences.Editor editor)
+    {
+    mMovesController.savePreferences(key,editor);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restoreMovePreferences(PlayActivity act, String key, SharedPreferences preferences)
+    {
+    mMovesController.restorePreferences(act,key,preferences);
+    }
+  }
diff --git a/src/main/java/org/distorted/play/ScreenDone.java b/src/main/java/org/distorted/play/ScreenDone.java
new file mode 100644
index 00000000..6be311fc
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenDone.java
@@ -0,0 +1,111 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import androidx.fragment.app.FragmentManager;
+
+import org.distorted.dialogs.DialogNewRecord;
+import org.distorted.dialogs.DialogSolved;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenDone extends ScreenAbstract
+  {
+  private TransparentImageButton mBackButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PlayActivity act)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PlayActivity act)
+    {
+    float width = act.getScreenWidthInPixels();
+    float titleSize = width*PlayActivity.TITLE_TEXT_SIZE;
+    LayoutInflater inflater = act.getLayoutInflater();
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+    TextView label = (TextView)inflater.inflate(R.layout.upper_text, null);
+    label.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
+    label.setText(R.string.solved);
+    layoutTop.addView(label);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,2);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+
+        FragmentManager mana = act.getSupportFragmentManager();
+        DialogNewRecord diag1 = (DialogNewRecord) mana.findFragmentByTag(DialogNewRecord.getDialogTag());
+        DialogSolved diag2 = (DialogSolved) mana.findFragmentByTag(DialogSolved.getDialogTag());
+
+        if( diag1 !=null ) diag1.dismiss();
+        if( diag2 !=null ) diag2.dismiss();
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/play/ScreenFree.java b/src/main/java/org/distorted/play/ScreenFree.java
new file mode 100644
index 00000000..03406ed6
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenFree.java
@@ -0,0 +1,140 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectControl;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenFree extends ScreenBase
+  {
+  private TransparentImageButton mBackButton, mScrambleButton, mSolveButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PlayActivity act)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PlayActivity act)
+    {
+    int width = act.getScreenWidthInPixels();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(width/4, LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams(width/2, LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(width/4, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    // TOP ////////////////////////////
+    setupSolveButton(act);
+    setupScrambleButton(act);
+
+    LinearLayout layoutUpper = act.findViewById(R.id.upperBar);
+
+    LinearLayout layoutLeftU = new LinearLayout(act);
+    layoutLeftU.setLayoutParams(paramsL);
+    LinearLayout layoutMidU  = new LinearLayout(act);
+    layoutMidU.setLayoutParams(paramsM);
+    LinearLayout layoutRightU= new LinearLayout(act);
+    layoutRightU.setLayoutParams(paramsR);
+
+    layoutLeftU.addView(mSolveButton);
+    layoutRightU.addView(mScrambleButton);
+
+    layoutUpper.removeAllViews();
+    layoutUpper.addView(layoutLeftU);
+    layoutUpper.addView(layoutMidU);
+    layoutUpper.addView(layoutRightU);
+
+    // BOT ////////////////////////////
+    setupBackButton(act);
+    createBottomPane(act,mBackButton);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_smallback,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        act.finish();
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupSolveButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mSolveButton = new TransparentImageButton(act,R.drawable.ui_cube_solve,params);
+
+    mSolveButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        control.solveObject();
+        mMovesController.clearMoves(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupScrambleButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mScrambleButton = new TransparentImageButton(act,R.drawable.ui_cube_scramble,params);
+
+    mScrambleButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        int duration = BaseEffect.Type.FAST_SCRAMBLE.getDuration();
+        int numScrambles = act.getNumScrambles();
+        control.fastScrambleObject(duration,numScrambles);
+        mMovesController.clearMoves(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/play/ScreenList.java b/src/main/java/org/distorted/play/ScreenList.java
new file mode 100644
index 00000000..d562bb07
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenList.java
@@ -0,0 +1,154 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
+import static org.distorted.objectlib.main.ObjectControl.MODE_NOTHING;
+import static org.distorted.objectlib.main.ObjectControl.MODE_ROTATE;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum ScreenList
+  {
+  FREE ( null , MODE_ROTATE , new ScreenFree()       ),
+  SCRA ( null , MODE_NOTHING, new ScreenScrambling() ),
+  READ ( null , MODE_ROTATE , new ScreenReady()      ),
+  SOLV ( null , MODE_ROTATE , new ScreenSolving()    ),
+  DONE ( null , MODE_DRAG   , new ScreenDone()       ),
+  ;
+
+  public static final int LENGTH = values().length;
+  private static final ScreenList[] screens;
+  private final ScreenList mBack;
+  private final int mMode;
+  private final ScreenAbstract mClass;
+
+  private static ScreenList mCurrScreen;
+
+  static
+    {
+    int i = 0;
+    screens = new ScreenList[LENGTH];
+    for(ScreenList state: ScreenList.values()) screens[i++] = state;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreen(int ordinal)
+    {
+    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : SCRA;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreenFromName(String name)
+    {
+    for(int i=0; i<LENGTH; i++)
+      {
+      if( name.equals(screens[i].name()) )
+        {
+        return screens[i];
+        }
+      }
+
+    return SCRA;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getCurrentScreen()
+    {
+    return mCurrScreen;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getMode()
+    {
+    return mCurrScreen.mMode;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void savePreferences(SharedPreferences.Editor editor)
+    {
+    editor.putString("curr_state_name", mCurrScreen.name() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void restorePreferences(SharedPreferences preferences)
+    {
+    String currScreenName = preferences.getString("curr_state_name", ScreenList.SCRA.name() );
+    mCurrScreen = getScreenFromName(currScreenName);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void goBack(PlayActivity act)
+    {
+    switchScreen(act, mCurrScreen.mBack );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void setScreen(PlayActivity act)
+    {
+    mCurrScreen.enterScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void switchScreen(PlayActivity act, ScreenList next)
+    {
+    if( next!=null )
+      {
+      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
+      next.enterScreen(act);
+      mCurrScreen = next;
+      }
+    else
+      {
+      act.finish();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
+    {
+    mBack = back;
+    mMode = mode;
+    mClass= clazz;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScreenAbstract getScreenClass()
+    {
+    return mClass;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void leaveScreen(PlayActivity act)
+    {
+    mClass.leaveScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enterScreen(PlayActivity act)
+    {
+    mClass.enterScreen(act);
+    }
+  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/play/ScreenReady.java b/src/main/java/org/distorted/play/ScreenReady.java
new file mode 100644
index 00000000..d37c70a3
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenReady.java
@@ -0,0 +1,100 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenReady extends ScreenAbstract
+  {
+  private TransparentImageButton mBackButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PlayActivity act)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PlayActivity act)
+    {
+    float width = act.getScreenWidthInPixels();
+    float titleSize = width*PlayActivity.TITLE_TEXT_SIZE;
+    LayoutInflater inflater = act.getLayoutInflater();
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+    TextView label = (TextView)inflater.inflate(R.layout.upper_text, null);
+    label.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
+    label.setText(R.string.ready);
+    layoutTop.addView(label);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,2);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/play/ScreenScrambling.java b/src/main/java/org/distorted/play/ScreenScrambling.java
new file mode 100644
index 00000000..cfbb58ca
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenScrambling.java
@@ -0,0 +1,89 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+import android.view.View;
+import android.widget.LinearLayout;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenScrambling extends ScreenAbstract
+  {
+  private TransparentImageButton mBackButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PlayActivity act)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PlayActivity act)
+    {
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,2);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+
+    }
+  }
diff --git a/src/main/java/org/distorted/play/ScreenSolving.java b/src/main/java/org/distorted/play/ScreenSolving.java
new file mode 100644
index 00000000..aa0cc63b
--- /dev/null
+++ b/src/main/java/org/distorted/play/ScreenSolving.java
@@ -0,0 +1,191 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.play;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.dialogs.DialogAbandon;
+import org.distorted.helpers.RubikScores;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSolving extends ScreenBase
+  {
+  private static final int MOVES_THRESHHOLD = 10;
+
+  private TextView mTime;
+  private Timer mTimer;
+  private long mStartTime;
+  private boolean mRunning;
+  private final RubikScores mScores;
+  private long mElapsed;
+  private int mLevel;
+  private TransparentImageButton mBackButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenSolving()
+    {
+    mScores = RubikScores.getInstance();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(PlayActivity act)
+    {
+    stopCounting();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final PlayActivity act)
+    {
+    mLevel = act.getLevel();
+    float width = act.getScreenWidthInPixels();
+    float titleSize  = width*PlayActivity.TITLE_TEXT_SIZE;
+
+    startCounting(act);
+
+    LayoutInflater inflater = act.getLayoutInflater();
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+    mTime = (TextView)inflater.inflate(R.layout.upper_text, null);
+    int elapsed = (int)mElapsed/1000;
+    mTime.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
+    mTime.setText(act.getString(R.string.tm_placeholder,elapsed/60,elapsed%60));
+    layoutTop.addView(mTime);
+
+    setupBackButton(act);
+    createBottomPane(act,mBackButton);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PlayActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( mMovesController.getNumMoves() > MOVES_THRESHHOLD )
+          {
+          DialogAbandon abaDiag = new DialogAbandon();
+          abaDiag.show(act.getSupportFragmentManager(), null);
+          }
+        else
+          {
+          ScreenList.goBack(act);
+          }
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+    stopCounting();
+    mElapsed = System.currentTimeMillis()-mStartTime;
+    editor.putLong("stateSolving_elapsed" , mElapsed);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+    mElapsed = preferences.getLong("stateSolving_elapsed" , 0 );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void startCounting(final PlayActivity act)
+    {
+    if( !mRunning )
+      {
+      mRunning = true;
+      mStartTime = System.currentTimeMillis() - mElapsed;
+      mTimer = new Timer();
+
+      mTimer.scheduleAtFixedRate(new TimerTask()
+        {
+        @Override
+        public void run()
+          {
+          act.runOnUiThread(new Runnable()
+            {
+            @Override
+            public void run()
+              {
+              int elapsed = (int)(System.currentTimeMillis()-mStartTime)/1000;
+              mTime.setText(act.getString(R.string.tm_placeholder,elapsed/60,elapsed%60));
+              }
+            });
+          }
+        }, 0, 1000);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void stopCounting()
+    {
+    if( mTimer!=null )
+      {
+      mTimer.cancel();
+      mTimer = null;
+      }
+
+    mRunning = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int stopTimerAndGetRecord()
+    {
+    if( mRunning )
+      {
+      stopCounting();
+      mElapsed = System.currentTimeMillis()-mStartTime;
+      return (int)mElapsed;
+      }
+
+    return 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int setRecord(int object)
+    {
+    return mScores.setRecord(object, mLevel, (int)mElapsed);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void resetElapsed()
+    {
+    mElapsed = 0;
+    }
+  }
diff --git a/src/main/java/org/distorted/playui/PlayActivity.java b/src/main/java/org/distorted/playui/PlayActivity.java
deleted file mode 100644
index bf5dc273..00000000
--- a/src/main/java/org/distorted/playui/PlayActivity.java
+++ /dev/null
@@ -1,313 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2022 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import java.io.InputStream;
-
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.DisplayCutout;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import com.google.firebase.analytics.FirebaseAnalytics;
-
-import org.distorted.dialogs.DialogScores;
-import org.distorted.helpers.RubikScores;
-import org.distorted.helpers.BaseActivity;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.objectlib.main.InitAssets;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-import org.distorted.dialogs.DialogError;
-import org.distorted.helpers.RubikFiles;
-import org.distorted.main.R;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
-import org.distorted.os.OSInterface;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PlayActivity extends BaseActivity implements DialogScores.ScoresInvoker
-{
-    public static final float TITLE_TEXT_SIZE= 0.060f;
-    private static final int ACTIVITY_NUMBER = 6;
-    private static final float RATIO_INSET   = 0.09f;
-
-    private static final String KEY_FREE = "movesController_free";
-    private static final String KEY_SOLV = "movesController_solv";
-
-    private String mObjectName;
-    private int mNumScrambles;
-    private boolean mObjectLocal;
-    private int mObjectOrdinal;
-    private int mLevel;
-    private boolean mModeFree;
-    private boolean mJustStarted;
-    private FirebaseAnalytics mFirebaseAnalytics;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    protected void onCreate(Bundle savedState)
-      {
-      super.onCreate(savedState);
-      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
-      setContentView(R.layout.play);
-
-      mJustStarted = true;
-      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
-
-      Bundle b = getIntent().getExtras();
-
-      if( b!=null )
-        {
-        mLevel         = b.getInt("level");
-        mObjectName    = b.getString("name");
-        mNumScrambles  = b.getInt("scrambles");
-        mObjectLocal   = b.getBoolean("local");
-        mObjectOrdinal = b.getInt("ordinal");
-        }
-      else
-        {
-        mLevel = -1;
-        mObjectName = "";
-        mNumScrambles = 0;
-        mObjectLocal = true;
-        mObjectOrdinal = 0;
-        }
-
-      mModeFree = (mLevel<0);
-
-      computeScreenDimensions();
-      hideNavigationBar();
-      cutoutHack();
-      computeUpperBarHeight(RATIO_BAR);
-      computeLowerBarHeight(RATIO_BAR);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onAttachedToWindow()
-      {
-      super.onAttachedToWindow();
-
-      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
-        {
-        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
-        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
-
-        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
-        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
-        paramsHid.height = (int)(insetHeight*RATIO_INSET);
-        layoutHid.setLayoutParams(paramsHid);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onPause() 
-      {
-      super.onPause();
-      PlayView view = findViewById(R.id.playView);
-      view.onPause();
-      savePreferences();
-      DistortedLibrary.onPause(ACTIVITY_NUMBER);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onResume() 
-      {
-      super.onResume();
-      DistortedLibrary.onResume(ACTIVITY_NUMBER);
-      PlayView view = findViewById(R.id.playView);
-      ObjectControl control = view.getObjectControl();
-      view.onResume();
-
-      restorePreferences();
-
-      ScreenList sl =  mJustStarted ?
-                      (mModeFree ? ScreenList.FREE : ScreenList.SCRA) :
-                      ScreenList.getCurrentScreen();
-
-      ScreenList.switchScreen(this,sl);
-
-      if( !mJustStarted ) restoreMoves();
-
-      if( mObjectName.length()>0 )
-        {
-        changeIfDifferent(mObjectName,mObjectLocal,mObjectOrdinal,control);
-        }
-
-      if( mJustStarted && !mModeFree )
-        {
-        control.scrambleObject(mNumScrambles);
-        }
-
-      mJustStarted = false;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onDestroy() 
-      {
-      super.onDestroy();
-      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void savePreferences()
-    {
-    SharedPreferences.Editor editor = mPreferences.edit();
-
-    for( int i=0; i<ScreenList.LENGTH; i++ )
-      {
-      ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
-      }
-
-    ScreenList.savePreferences(editor);
-
-    ScreenList curr = ScreenList.getCurrentScreen();
-
-    if( curr==ScreenList.FREE )
-      {
-      ScreenFree free = (ScreenFree) ScreenList.FREE.getScreenClass();
-      free.saveMovePreferences(KEY_FREE,editor);
-      }
-    if( curr==ScreenList.SOLV )
-      {
-      ScreenSolving solv = (ScreenSolving) ScreenList.SOLV.getScreenClass();
-      solv.saveMovePreferences(KEY_SOLV,editor);
-      }
-
-    PlayView view = findViewById(R.id.playView);
-    OSInterface os = view.getInterface();
-    os.setEditor(editor);
-    view.getObjectControl().savePreferences();
-
-    RubikScores scores = RubikScores.getInstance();
-    scores.savePreferences(editor);
-
-    editor.apply();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void restorePreferences()
-    {
-    for( int i=0; i<ScreenList.LENGTH; i++)
-      {
-      ScreenList.getScreen(i).getScreenClass().restorePreferences(mPreferences);
-      }
-
-    if( !mJustStarted ) ScreenList.restorePreferences(mPreferences);
-
-    PlayView view = findViewById(R.id.playView);
-    OSInterface os = view.getInterface();
-    os.setPreferences(mPreferences);
-    view.getObjectControl().restorePreferences();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void restoreMoves()
-    {
-    ScreenList curr = ScreenList.getCurrentScreen();
-
-    if( curr==ScreenList.FREE )
-      {
-      ScreenFree free = (ScreenFree) ScreenList.FREE.getScreenClass();
-      free.restoreMovePreferences(this,KEY_FREE,mPreferences);
-      }
-    if( curr==ScreenList.SOLV )
-      {
-      ScreenSolving solv = (ScreenSolving) ScreenList.SOLV.getScreenClass();
-      solv.restoreMovePreferences(this,KEY_SOLV,mPreferences);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void OpenGLError()
-      {
-      DialogError errDiag = new DialogError();
-      errDiag.show(getSupportFragmentManager(), null);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void changeIfDifferent(String name, boolean local, int ordinal, ObjectControl control)
-      {
-      if( local )
-        {
-        RubikFiles files = RubikFiles.getInstance();
-        int iconMode = TwistyObject.MODE_NORM;
-        InputStream jsonStream = files.openFile(this, name+"_object.json");
-        InitAssets asset = new InitAssets(jsonStream, null, null);
-        control.changeIfDifferent(ordinal,name,iconMode,asset);
-        }
-      else
-        {
-        RubikObject object = RubikObjectList.getObject(ordinal);
-        int iconMode = TwistyObject.MODE_NORM;
-        InputStream jsonStream = object==null ? null : object.getObjectStream(this);
-        InputStream meshStream = object==null ? null : object.getMeshStream(this);
-        PlayView view = findViewById(R.id.playView);
-        OSInterface os = view.getInterface();
-        InitAssets asset = new InitAssets(jsonStream, meshStream, os);
-        control.changeIfDifferent(ordinal, name, iconMode, asset);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public FirebaseAnalytics getAnalytics()
-      {
-      return mFirebaseAnalytics;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getNumScrambles()
-      {
-      return mNumScrambles;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getLevel()
-      {
-      return mLevel;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getObjectOrdinal()
-      {
-      return mObjectOrdinal;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public ObjectControl getControl()
-      {
-      PlayView view = findViewById(R.id.playView);
-      return view.getObjectControl();
-      }
-}
diff --git a/src/main/java/org/distorted/playui/PlayLibInterface.java b/src/main/java/org/distorted/playui/PlayLibInterface.java
deleted file mode 100644
index e9fb1c81..00000000
--- a/src/main/java/org/distorted/playui/PlayLibInterface.java
+++ /dev/null
@@ -1,454 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import static org.distorted.helpers.RubikScores.RECORD_FIRST;
-import static org.distorted.helpers.RubikScores.RECORD_NEW;
-import static org.distorted.helpers.RubikScores.RECORD_NOT_NEW;
-
-import android.os.Bundle;
-
-import androidx.annotation.NonNull;
-
-import java.lang.ref.WeakReference;
-
-import com.google.android.play.core.review.ReviewInfo;
-import com.google.android.play.core.review.ReviewManager;
-import com.google.android.play.core.review.ReviewManagerFactory;
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.OnFailureListener;
-import com.google.android.gms.tasks.Task;
-import com.google.firebase.analytics.FirebaseAnalytics;
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.dialogs.DialogNewRecord;
-import org.distorted.dialogs.DialogScoresView;
-import org.distorted.dialogs.DialogSolved;
-import org.distorted.helpers.RubikNetwork;
-import org.distorted.helpers.RubikScores;
-import org.distorted.library.message.EffectMessageSender;
-import org.distorted.objectlib.helpers.BlockController;
-import org.distorted.objectlib.helpers.ObjectLibInterface;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
-import org.distorted.main.BuildConfig;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PlayLibInterface implements ObjectLibInterface
-{
-  private final WeakReference<PlayActivity> mAct;
-  private int mIsNewRecord;
-  private int mNewRecord;
-  private boolean mReviewAsked;
-  private int mNumRotations, mNumScrambles;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  PlayLibInterface(PlayActivity act)
-    {
-    mAct = new WeakReference<>(act);
-    mReviewAsked = false;
-    mNumRotations = 0;
-    mNumScrambles = 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onObjectCreated(long time) { }
-  public void onReplaceModeDown(int cubit, int face) { }
-  public void onReplaceModeUp() { }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void analyticsReport(PlayActivity act, String message, String name, long timeBegin)
-    {
-    long elapsed = System.currentTimeMillis() - timeBegin;
-    String msg = message+" startTime: "+timeBegin+" elapsed: "+elapsed+" name: "+name;
-
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.d("libInterface", msg);
-      }
-    else
-      {
-      FirebaseAnalytics analytics = act.getAnalytics();
-
-      if( analytics!=null )
-        {
-        Bundle bundle = new Bundle();
-        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, msg);
-        analytics.logEvent(FirebaseAnalytics.Event.SHARE, bundle);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportRecord(PlayActivity act, long startTime, long endTime, String debug, int scrambleNum)
-    {
-    RubikScores scores  = RubikScores.getInstance();
-    int objectOrdinal = act.getObjectOrdinal();
-    String name = scores.getName();
-    RubikObject obj = RubikObjectList.getObject(objectOrdinal);
-    String objName = obj==null ? "NULL" : obj.getUpperName();
-
-    String record = objName+" time "+mNewRecord+" isNew: "+mIsNewRecord+" scrambleNum: "+scrambleNum;
-
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.e("libInterface", debug);
-      android.util.Log.e("libInterface", name);
-      android.util.Log.e("libInterface", record);
-      }
-    else
-      {
-      if( scrambleNum>=9 && mNewRecord<300*scrambleNum )
-        {
-        long timeNow = System.currentTimeMillis();
-        long elapsed = timeNow - startTime;
-        String suspicious ="start"+startTime+"end"+endTime+"elapsed"+elapsed+"obj"+objName+"record"+mNewRecord+"scrambles"+scrambleNum+debug;
-        RubikNetwork network = RubikNetwork.getInstance();
-        network.suspicious(suspicious,act);
-        }
-
-      FirebaseAnalytics analytics = act.getAnalytics();
-
-      if( analytics!=null )
-        {
-        Bundle bundle = new Bundle();
-        bundle.putString(FirebaseAnalytics.Param.CONTENT_TYPE, debug);
-        bundle.putString(FirebaseAnalytics.Param.CHARACTER, name);
-        bundle.putString(FirebaseAnalytics.Param.LEVEL, record);
-        analytics.logEvent(FirebaseAnalytics.Event.LEVEL_UP, bundle);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private Bundle createDialogBundle()
-    {
-    Bundle bundle = new Bundle();
-    String arg = DialogScoresView.formatRecord(mNewRecord);
-    bundle.putString("argument", arg );
-    return bundle;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void requestReview(PlayActivity act)
-    {
-    android.util.Log.e("D", "ASKING FOR REVIEW");
-
-    mReviewAsked = true;
-    final String name = RubikScores.getInstance().getName();
-    final long timeBegin = System.currentTimeMillis();
-    final ReviewManager manager = ReviewManagerFactory.create(act);
-    Task<ReviewInfo> request = manager.requestReviewFlow();
-
-    request.addOnCompleteListener(new OnCompleteListener<ReviewInfo>()
-      {
-      @Override
-      public void onComplete (@NonNull Task<ReviewInfo> task)
-        {
-        if (task.isSuccessful())
-          {
-          ReviewInfo reviewInfo = task.getResult();
-          Task<Void> flow = manager.launchReviewFlow(act, reviewInfo);
-
-          flow.addOnFailureListener(new OnFailureListener()
-            {
-            @Override
-            public void onFailure(Exception e)
-            {
-            analyticsReport(act,"Failed", name, timeBegin);
-            }
-            });
-
-          flow.addOnCompleteListener(new OnCompleteListener<Void>()
-            {
-            @Override
-            public void onComplete(@NonNull Task<Void> task)
-              {
-              analyticsReport(act,"Complete", name, timeBegin);
-              }
-            });
-          }
-        else analyticsReport(act,"Not Successful", name, timeBegin);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onScrambleEffectFinished()
-    {
-    if( ScreenList.getCurrentScreen()==ScreenList.SCRA )
-      {
-      PlayActivity act = mAct.get();
-      RubikScores.getInstance().incrementNumPlays();
-
-      act.runOnUiThread(new Runnable()
-        {
-        @Override
-        public void run()
-          {
-          ScreenList.switchScreen(act,ScreenList.READ);
-          ObjectControl control = act.getControl();
-          control.unblockEverything();
-          }
-        });
-      }
-
-    mNumScrambles++;
-
-    if( mNumScrambles==10 && !mReviewAsked )
-      {
-      PlayActivity act = mAct.get();
-      requestReview(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onRemoveRotation(int axis, int row, int angle)
-    {
-    mNumRotations++;
-    PlayActivity act = mAct.get();
-
-    if( act!=null )
-      {
-      ScreenList screen = ScreenList.getCurrentScreen();
-
-      if( screen==ScreenList.FREE ||
-          screen==ScreenList.SOLV  ) ((ScreenBase)screen.getScreenClass()).addMove(act,axis,row,angle);
-      }
-
-    if( mNumRotations==40 && !mReviewAsked )
-      {
-      requestReview(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onBeginRotation()
-    {
-    if( ScreenList.getCurrentScreen()==ScreenList.READ )
-      {
-      ScreenSolving solving = (ScreenSolving) ScreenList.SOLV.getScreenClass();
-      solving.resetElapsed();
-      PlayActivity act = mAct.get();
-      ScreenList.switchScreen( act,ScreenList.SOLV );
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void failedToDrag()
-    {
-    PlayActivity act = mAct.get();
-
-    if( act!=null )
-      {
-      ScreenList screen = ScreenList.getCurrentScreen();
-
-      if( screen==ScreenList.FREE ||
-          screen==ScreenList.SOLV ||
-          screen==ScreenList.SCRA  ) ((ScreenBase)screen.getScreenClass()).reddenLock(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onSolved()
-    {
-    if( ScreenList.getCurrentScreen()==ScreenList.SOLV )
-      {
-      PlayActivity act = mAct.get();
-      ObjectControl control = act.getControl();
-      TwistyObject obj = control.getObject();
-      boolean submittable = obj.isSubmittable();
-      int objectOrdinal = act.getObjectOrdinal();
-      ScreenSolving solving = (ScreenSolving)ScreenList.SOLV.getScreenClass();
-      mNewRecord = solving.stopTimerAndGetRecord();
-      mIsNewRecord = submittable ? solving.setRecord(objectOrdinal) : RECORD_NOT_NEW;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum)
-    {
-    if( ScreenList.getCurrentScreen()==ScreenList.SOLV )
-      {
-      PlayActivity act = mAct.get();
-      ObjectControl control = act.getControl();
-      TwistyObject obj = control.getObject();
-      boolean submittable = obj.isSubmittable();
-
-      if( submittable ) reportRecord(act,startTime,endTime,debug,scrambleNum);
-
-      RubikScores scores = RubikScores.getInstance();
-      int numWins = scores.incrementNumWins();
-      int numRuns = scores.getNumRuns();
-
-      if( numRuns==3 || numRuns==6 || numWins==4 || numWins==20 || numWins==50 || numWins==80 || numWins==100)
-        {
-        requestReview(act);
-        }
-
-      switch(mIsNewRecord)
-        {
-        case RECORD_FIRST  :
-        case RECORD_NEW    : Bundle byes = createDialogBundle();
-                             DialogNewRecord dyes = new DialogNewRecord();
-                             dyes.setArguments(byes);
-                             dyes.show( act.getSupportFragmentManager(), DialogNewRecord.getDialogTag());
-                             break;
-        case RECORD_NOT_NEW: Bundle bno = createDialogBundle();
-                             DialogSolved dno = new DialogSolved();
-                             dno.setArguments(bno);
-                             dno.show( act.getSupportFragmentManager(), DialogSolved.getDialogTag());
-        break;
-        }
-
-      act.runOnUiThread(new Runnable()
-        {
-        @Override
-        public void run()
-          {
-          ScreenList.switchScreen( act,ScreenList.DONE );
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportProblem(String problem, boolean reportException)
-    {
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.e("interface", problem);
-      }
-    else
-      {
-      if( reportException )
-        {
-        Exception ex = new Exception(problem);
-        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-        crashlytics.setCustomKey("problem" , problem);
-        crashlytics.recordException(ex);
-        }
-      else
-        {
-        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-        crashlytics.log(problem);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportScramblingProblem(int place, long pause, long resume, long time)
-    {
-    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("D", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause );
-      crashlytics.setCustomKey("resume", resume );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportRotationProblem(int place, long pause, long resume, long time)
-    {
-    String error = "ROTATION BLOCK "+place+" blocked for "+time;
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("D", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause );
-      crashlytics.setCustomKey("resume", resume);
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportThreadProblem(int place, long pause, long resume, long time)
-    {
-    String error = EffectMessageSender.reportState();
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("D", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause  );
-      crashlytics.setCustomKey("resume", resume );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
-    {
-    switch(type)
-      {
-      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
-      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
-      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportJSONError(String error, int ordinal)
-    {
-    RubikObject object = RubikObjectList.getObject(ordinal);
-    String name = object==null ? "NULL" : object.getUpperName();
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("name" , name );
-      crashlytics.setCustomKey("JSONerror", error );
-      crashlytics.recordException(ex);
-      }
-    }
-}
diff --git a/src/main/java/org/distorted/playui/PlayRenderer.java b/src/main/java/org/distorted/playui/PlayRenderer.java
deleted file mode 100644
index 5cbedd5a..00000000
--- a/src/main/java/org/distorted/playui/PlayRenderer.java
+++ /dev/null
@@ -1,111 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2022 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import java.io.InputStream;
-
-import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.opengles.GL10;
-
-import android.content.res.Resources;
-import android.opengl.GLSurfaceView;
-
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.VertexEffectQuaternion;
-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.objectlib.effects.BaseEffect;
-import org.distorted.objectlib.main.ObjectControl;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PlayRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
-{
-   private final PlayView mView;
-   private final Resources mResources;
-   private final DistortedScreen mScreen;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   PlayRenderer(PlayView v)
-     {
-     mView = v;
-     mResources = v.getResources();
-     mScreen = new DistortedScreen();
-
-     PlayActivity act = (PlayActivity)v.getContext();
-     act.setUpBackgroundColor(mScreen);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onDrawFrame(GL10 glUnused)
-     {
-     long time = System.currentTimeMillis();
-     mView.getObjectControl().preRender();
-     mScreen.render(time);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceChanged(GL10 glUnused, int width, int height)
-      {
-      mScreen.resize(width,height);
-      mView.setScreenSize(width,height);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   DistortedScreen getScreen()
-     {
-     return mScreen;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
-      {
-      DistortedLibrary.setMax(EffectType.VERTEX, ObjectControl.MAX_QUATS+1);
-      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
-
-      VertexEffectRotate.enable();
-      VertexEffectQuaternion.enable();
-      BaseEffect.Type.enableEffects();
-
-      DistortedLibrary.onSurfaceCreated(this,1);
-      DistortedLibrary.setCull(true);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void distortedException(Exception ex)
-     {
-     android.util.Log.e("Play", "unexpected exception: "+ex.getMessage() );
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public InputStream localFile(int fileID)
-      {
-      return mResources.openRawResource(fileID);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void logMessage(String message)
-      {
-      android.util.Log.e("Play", message );
-      }
-}
diff --git a/src/main/java/org/distorted/playui/PlayView.java b/src/main/java/org/distorted/playui/PlayView.java
deleted file mode 100644
index e8d14a87..00000000
--- a/src/main/java/org/distorted/playui/PlayView.java
+++ /dev/null
@@ -1,143 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import static org.distorted.objectlib.main.ObjectControl.MODE_ROTATE;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ConfigurationInfo;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObjectNode;
-import org.distorted.os.OSInterface;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class PlayView extends GLSurfaceView
-{
-    private ObjectControl mObjectController;
-    private OSInterface mInterface;
-    private PlayRenderer mRenderer;
-    private boolean mCreated;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setScreenSize(int width, int height)
-      {
-      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)) );
-      mObjectController.setObjectScale(1.00f);
-
-      if( !mCreated )
-        {
-        mCreated = true;
-        mObjectController.createNode(width,height);
-        TwistyObjectNode objectNode = mObjectController.getNode();
-        mRenderer.getScreen().attach(objectNode);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    ObjectControl getObjectControl()
-      {
-      return mObjectController;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    OSInterface getInterface()
-      {
-      return mInterface;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public PlayView(Context context, AttributeSet attrs)
-      {
-      super(context,attrs);
-
-      mCreated = false;
-
-      if(!isInEditMode())
-        {
-        PlayActivity act = (PlayActivity)context;
-        PlayLibInterface ref = new PlayLibInterface(act);
-        mInterface = new OSInterface(act,ref);
-        mObjectController = new ObjectControl(mInterface);
-        mObjectController.setRotateOnCreation(true);
-        mRenderer = new PlayRenderer(this);
-
-        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
-        try
-          {
-          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-          int esVersion = configurationInfo.reqGlEsVersion>>16;
-          setEGLContextClientVersion(esVersion);
-          setRenderer(mRenderer);
-          }
-        catch(Exception ex)
-          {
-          act.OpenGLError();
-
-          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
-          String version = GLES30.glGetString(GLES30.GL_VERSION);
-          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
-          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
-
-          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-          crashlytics.setCustomKey("GLSL Version"  , shading );
-          crashlytics.setCustomKey("GL version"    , version );
-          crashlytics.setCustomKey("GL Vendor "    , vendor  );
-          crashlytics.setCustomKey("GLSL renderer" , renderer);
-          crashlytics.recordException(ex);
-          }
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onPause()
-      {
-      super.onPause();
-      mObjectController.onPause();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onResume()
-      {
-      super.onResume();
-      mObjectController.onResume();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @SuppressLint("ClickableViewAccessibility")
-    @Override
-    public boolean onTouchEvent(MotionEvent event)
-      {
-      mInterface.setMotionEvent(event);
-      return mObjectController.onTouchEvent(MODE_ROTATE);
-      }
-}
-
diff --git a/src/main/java/org/distorted/playui/ScreenAbstract.java b/src/main/java/org/distorted/playui/ScreenAbstract.java
deleted file mode 100644
index 3bd3520f..00000000
--- a/src/main/java/org/distorted/playui/ScreenAbstract.java
+++ /dev/null
@@ -1,22 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class ScreenAbstract
-  {
-  abstract void enterScreen(PlayActivity act);
-  abstract void leaveScreen(PlayActivity act);
-  public abstract void savePreferences(SharedPreferences.Editor editor);
-  public abstract void restorePreferences(SharedPreferences preferences);
-  }
diff --git a/src/main/java/org/distorted/playui/ScreenBase.java b/src/main/java/org/distorted/playui/ScreenBase.java
deleted file mode 100644
index 7b1568f7..00000000
--- a/src/main/java/org/distorted/playui/ScreenBase.java
+++ /dev/null
@@ -1,104 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-import android.widget.LinearLayout;
-
-import org.distorted.helpers.LockController;
-import org.distorted.helpers.MovesController;
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-import org.distorted.objectlib.main.ObjectControl;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-abstract class ScreenBase extends ScreenAbstract
-  {
-  private final LockController mLockController;
-  protected MovesController mMovesController;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void createBottomPane(final PlayActivity act, TransparentImageButton butt)
-    {
-    mMovesController.clearMoves(act);
-
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(params);
-    LinearLayout layoutMid = new LinearLayout(act);
-    layoutMid.setLayoutParams(params);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(params);
-
-    ObjectControl control = act.getControl();
-    mMovesController.setupButton(act,control);
-    layoutLeft.addView(mMovesController.getButton());
-    mLockController.setupButton(act,control);
-    layoutMid.addView(mLockController.getButton());
-
-    if( butt !=null ) layoutRight.addView(butt);
-
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutMid);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setLockState(final PlayActivity act)
-    {
-    boolean locked = act.getControl().retLocked();
-    mLockController.setState(act,locked);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-
-  public ScreenBase()
-    {
-    mLockController = new LockController();
-    mMovesController= new MovesController();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void addMove(PlayActivity act, int axis, int row, int angle)
-    {
-    mMovesController.addMove(act,axis,row,angle);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reddenLock(final PlayActivity act)
-    {
-    ObjectControl control = act.getControl();
-    mLockController.reddenLock(act,control);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void saveMovePreferences(String key,SharedPreferences.Editor editor)
-    {
-    mMovesController.savePreferences(key,editor);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restoreMovePreferences(PlayActivity act, String key, SharedPreferences preferences)
-    {
-    mMovesController.restorePreferences(act,key,preferences);
-    }
-  }
diff --git a/src/main/java/org/distorted/playui/ScreenDone.java b/src/main/java/org/distorted/playui/ScreenDone.java
deleted file mode 100644
index 696680aa..00000000
--- a/src/main/java/org/distorted/playui/ScreenDone.java
+++ /dev/null
@@ -1,111 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import androidx.fragment.app.FragmentManager;
-
-import org.distorted.dialogs.DialogNewRecord;
-import org.distorted.dialogs.DialogSolved;
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenDone extends ScreenAbstract
-  {
-  private TransparentImageButton mBackButton;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(PlayActivity act)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final PlayActivity act)
-    {
-    float width = act.getScreenWidthInPixels();
-    float titleSize = width*PlayActivity.TITLE_TEXT_SIZE;
-    LayoutInflater inflater = act.getLayoutInflater();
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-    TextView label = (TextView)inflater.inflate(R.layout.upper_text, null);
-    label.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
-    label.setText(R.string.solved);
-    layoutTop.addView(label);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,2);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-
-        FragmentManager mana = act.getSupportFragmentManager();
-        DialogNewRecord diag1 = (DialogNewRecord) mana.findFragmentByTag(DialogNewRecord.getDialogTag());
-        DialogSolved diag2 = (DialogSolved) mana.findFragmentByTag(DialogSolved.getDialogTag());
-
-        if( diag1 !=null ) diag1.dismiss();
-        if( diag2 !=null ) diag2.dismiss();
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-
-    }
-  }
diff --git a/src/main/java/org/distorted/playui/ScreenFree.java b/src/main/java/org/distorted/playui/ScreenFree.java
deleted file mode 100644
index fbe86d08..00000000
--- a/src/main/java/org/distorted/playui/ScreenFree.java
+++ /dev/null
@@ -1,140 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-import org.distorted.objectlib.effects.BaseEffect;
-import org.distorted.objectlib.main.ObjectControl;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenFree extends ScreenBase
-  {
-  private TransparentImageButton mBackButton, mScrambleButton, mSolveButton;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(PlayActivity act)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final PlayActivity act)
-    {
-    int width = act.getScreenWidthInPixels();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(width/4, LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams(width/2, LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(width/4, LinearLayout.LayoutParams.MATCH_PARENT);
-
-    // TOP ////////////////////////////
-    setupSolveButton(act);
-    setupScrambleButton(act);
-
-    LinearLayout layoutUpper = act.findViewById(R.id.upperBar);
-
-    LinearLayout layoutLeftU = new LinearLayout(act);
-    layoutLeftU.setLayoutParams(paramsL);
-    LinearLayout layoutMidU  = new LinearLayout(act);
-    layoutMidU.setLayoutParams(paramsM);
-    LinearLayout layoutRightU= new LinearLayout(act);
-    layoutRightU.setLayoutParams(paramsR);
-
-    layoutLeftU.addView(mSolveButton);
-    layoutRightU.addView(mScrambleButton);
-
-    layoutUpper.removeAllViews();
-    layoutUpper.addView(layoutLeftU);
-    layoutUpper.addView(layoutMidU);
-    layoutUpper.addView(layoutRightU);
-
-    // BOT ////////////////////////////
-    setupBackButton(act);
-    createBottomPane(act,mBackButton);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_smallback,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        act.finish();
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupSolveButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mSolveButton = new TransparentImageButton(act,R.drawable.ui_cube_solve,params);
-
-    mSolveButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        control.solveObject();
-        mMovesController.clearMoves(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupScrambleButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mScrambleButton = new TransparentImageButton(act,R.drawable.ui_cube_scramble,params);
-
-    mScrambleButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        int duration = BaseEffect.Type.FAST_SCRAMBLE.getDuration();
-        int numScrambles = act.getNumScrambles();
-        control.fastScrambleObject(duration,numScrambles);
-        mMovesController.clearMoves(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-
-    }
-  }
diff --git a/src/main/java/org/distorted/playui/ScreenList.java b/src/main/java/org/distorted/playui/ScreenList.java
deleted file mode 100644
index 7679b019..00000000
--- a/src/main/java/org/distorted/playui/ScreenList.java
+++ /dev/null
@@ -1,154 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
-import static org.distorted.objectlib.main.ObjectControl.MODE_NOTHING;
-import static org.distorted.objectlib.main.ObjectControl.MODE_ROTATE;
-
-import android.content.SharedPreferences;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public enum ScreenList
-  {
-  FREE ( null , MODE_ROTATE , new ScreenFree()       ),
-  SCRA ( null , MODE_NOTHING, new ScreenScrambling() ),
-  READ ( null , MODE_ROTATE , new ScreenReady()      ),
-  SOLV ( null , MODE_ROTATE , new ScreenSolving()    ),
-  DONE ( null , MODE_DRAG   , new ScreenDone()       ),
-  ;
-
-  public static final int LENGTH = values().length;
-  private static final ScreenList[] screens;
-  private final ScreenList mBack;
-  private final int mMode;
-  private final ScreenAbstract mClass;
-
-  private static ScreenList mCurrScreen;
-
-  static
-    {
-    int i = 0;
-    screens = new ScreenList[LENGTH];
-    for(ScreenList state: ScreenList.values()) screens[i++] = state;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getScreen(int ordinal)
-    {
-    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : SCRA;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getScreenFromName(String name)
-    {
-    for(int i=0; i<LENGTH; i++)
-      {
-      if( name.equals(screens[i].name()) )
-        {
-        return screens[i];
-        }
-      }
-
-    return SCRA;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getCurrentScreen()
-    {
-    return mCurrScreen;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getMode()
-    {
-    return mCurrScreen.mMode;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void savePreferences(SharedPreferences.Editor editor)
-    {
-    editor.putString("curr_state_name", mCurrScreen.name() );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void restorePreferences(SharedPreferences preferences)
-    {
-    String currScreenName = preferences.getString("curr_state_name", ScreenList.SCRA.name() );
-    mCurrScreen = getScreenFromName(currScreenName);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void goBack(PlayActivity act)
-    {
-    switchScreen(act, mCurrScreen.mBack );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void setScreen(PlayActivity act)
-    {
-    mCurrScreen.enterScreen(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void switchScreen(PlayActivity act, ScreenList next)
-    {
-    if( next!=null )
-      {
-      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
-      next.enterScreen(act);
-      mCurrScreen = next;
-      }
-    else
-      {
-      act.finish();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
-    {
-    mBack = back;
-    mMode = mode;
-    mClass= clazz;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ScreenAbstract getScreenClass()
-    {
-    return mClass;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void leaveScreen(PlayActivity act)
-    {
-    mClass.leaveScreen(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void enterScreen(PlayActivity act)
-    {
-    mClass.enterScreen(act);
-    }
-  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/playui/ScreenReady.java b/src/main/java/org/distorted/playui/ScreenReady.java
deleted file mode 100644
index 0f338924..00000000
--- a/src/main/java/org/distorted/playui/ScreenReady.java
+++ /dev/null
@@ -1,100 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenReady extends ScreenAbstract
-  {
-  private TransparentImageButton mBackButton;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(PlayActivity act)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final PlayActivity act)
-    {
-    float width = act.getScreenWidthInPixels();
-    float titleSize = width*PlayActivity.TITLE_TEXT_SIZE;
-    LayoutInflater inflater = act.getLayoutInflater();
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-    TextView label = (TextView)inflater.inflate(R.layout.upper_text, null);
-    label.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
-    label.setText(R.string.ready);
-    layoutTop.addView(label);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,2);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-
-    }
-  }
diff --git a/src/main/java/org/distorted/playui/ScreenScrambling.java b/src/main/java/org/distorted/playui/ScreenScrambling.java
deleted file mode 100644
index 94d38370..00000000
--- a/src/main/java/org/distorted/playui/ScreenScrambling.java
+++ /dev/null
@@ -1,89 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-import android.view.View;
-import android.widget.LinearLayout;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenScrambling extends ScreenAbstract
-  {
-  private TransparentImageButton mBackButton;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(PlayActivity act)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final PlayActivity act)
-    {
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,2);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-
-    }
-  }
diff --git a/src/main/java/org/distorted/playui/ScreenSolving.java b/src/main/java/org/distorted/playui/ScreenSolving.java
deleted file mode 100644
index 772d78e8..00000000
--- a/src/main/java/org/distorted/playui/ScreenSolving.java
+++ /dev/null
@@ -1,191 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.playui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.dialogs.DialogAbandon;
-import org.distorted.helpers.RubikScores;
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-
-import java.util.Timer;
-import java.util.TimerTask;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenSolving extends ScreenBase
-  {
-  private static final int MOVES_THRESHHOLD = 10;
-
-  private TextView mTime;
-  private Timer mTimer;
-  private long mStartTime;
-  private boolean mRunning;
-  private final RubikScores mScores;
-  private long mElapsed;
-  private int mLevel;
-  private TransparentImageButton mBackButton;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  ScreenSolving()
-    {
-    mScores = RubikScores.getInstance();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(PlayActivity act)
-    {
-    stopCounting();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final PlayActivity act)
-    {
-    mLevel = act.getLevel();
-    float width = act.getScreenWidthInPixels();
-    float titleSize  = width*PlayActivity.TITLE_TEXT_SIZE;
-
-    startCounting(act);
-
-    LayoutInflater inflater = act.getLayoutInflater();
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-    mTime = (TextView)inflater.inflate(R.layout.upper_text, null);
-    int elapsed = (int)mElapsed/1000;
-    mTime.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
-    mTime.setText(act.getString(R.string.tm_placeholder,elapsed/60,elapsed%60));
-    layoutTop.addView(mTime);
-
-    setupBackButton(act);
-    createBottomPane(act,mBackButton);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final PlayActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( mMovesController.getNumMoves() > MOVES_THRESHHOLD )
-          {
-          DialogAbandon abaDiag = new DialogAbandon();
-          abaDiag.show(act.getSupportFragmentManager(), null);
-          }
-        else
-          {
-          ScreenList.goBack(act);
-          }
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-    stopCounting();
-    mElapsed = System.currentTimeMillis()-mStartTime;
-    editor.putLong("stateSolving_elapsed" , mElapsed);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-    mElapsed = preferences.getLong("stateSolving_elapsed" , 0 );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void startCounting(final PlayActivity act)
-    {
-    if( !mRunning )
-      {
-      mRunning = true;
-      mStartTime = System.currentTimeMillis() - mElapsed;
-      mTimer = new Timer();
-
-      mTimer.scheduleAtFixedRate(new TimerTask()
-        {
-        @Override
-        public void run()
-          {
-          act.runOnUiThread(new Runnable()
-            {
-            @Override
-            public void run()
-              {
-              int elapsed = (int)(System.currentTimeMillis()-mStartTime)/1000;
-              mTime.setText(act.getString(R.string.tm_placeholder,elapsed/60,elapsed%60));
-              }
-            });
-          }
-        }, 0, 1000);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void stopCounting()
-    {
-    if( mTimer!=null )
-      {
-      mTimer.cancel();
-      mTimer = null;
-      }
-
-    mRunning = false;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int stopTimerAndGetRecord()
-    {
-    if( mRunning )
-      {
-      stopCounting();
-      mElapsed = System.currentTimeMillis()-mStartTime;
-      return (int)mElapsed;
-      }
-
-    return 0;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int setRecord(int object)
-    {
-    return mScores.setRecord(object, mLevel, (int)mElapsed);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void resetElapsed()
-    {
-    mElapsed = 0;
-    }
-  }
diff --git a/src/main/java/org/distorted/solvers/ScreenAbstract.java b/src/main/java/org/distorted/solvers/ScreenAbstract.java
new file mode 100644
index 00000000..bd549465
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/ScreenAbstract.java
@@ -0,0 +1,22 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public abstract class ScreenAbstract
+  {
+  abstract void enterScreen(SolverActivity act);
+  abstract void leaveScreen(SolverActivity act);
+  public abstract void savePreferences(SharedPreferences.Editor editor);
+  public abstract void restorePreferences(SharedPreferences preferences);
+  }
diff --git a/src/main/java/org/distorted/solvers/ScreenList.java b/src/main/java/org/distorted/solvers/ScreenList.java
new file mode 100644
index 00000000..9289f9cd
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/ScreenList.java
@@ -0,0 +1,151 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
+import static org.distorted.objectlib.main.ObjectControl.MODE_REPLACE;
+
+import android.content.SharedPreferences;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public enum ScreenList
+  {
+  SVER ( null , MODE_REPLACE, new ScreenSetupPosition()        ),
+  SOLU ( SVER , MODE_DRAG   , new ScreenSolutionSinglephased() ),
+  PHAS ( SVER , MODE_DRAG   , new ScreenSolutionMultiphased()  ),
+  ;
+
+  public static final int LENGTH = values().length;
+  private static final ScreenList[] screens;
+  private final ScreenList mBack;
+  private final int mMode;
+  private final ScreenAbstract mClass;
+
+  private static ScreenList mCurrScreen;
+
+  static
+    {
+    int i = 0;
+    screens = new ScreenList[LENGTH];
+    for(ScreenList state: ScreenList.values()) screens[i++] = state;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreen(int ordinal)
+    {
+    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : SVER;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getScreenFromName(String name)
+    {
+    for(int i=0; i<LENGTH; i++)
+      {
+      if( name.equals(screens[i].name()) )
+        {
+        return screens[i];
+        }
+      }
+
+    return SVER;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static ScreenList getCurrentScreen()
+    {
+    return mCurrScreen;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static int getMode()
+    {
+    return mCurrScreen.mMode;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void savePreferences(SharedPreferences.Editor editor)
+    {
+    editor.putString("curr_state_name", mCurrScreen.name() );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void restorePreferences(SharedPreferences preferences)
+    {
+    String currScreenName = preferences.getString("curr_state_name", ScreenList.SVER.name() );
+    mCurrScreen = getScreenFromName(currScreenName);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void goBack(SolverActivity act)
+    {
+    switchScreen(act, mCurrScreen.mBack );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void setScreen(SolverActivity act)
+    {
+    mCurrScreen.enterScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void switchScreen(SolverActivity act, ScreenList next)
+    {
+    if( next!=null )
+      {
+      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
+      next.enterScreen(act);
+      mCurrScreen = next;
+      }
+    else
+      {
+      act.finish();
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
+    {
+    mBack = back;
+    mMode = mode;
+    mClass= clazz;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ScreenAbstract getScreenClass()
+    {
+    return mClass;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void leaveScreen(SolverActivity act)
+    {
+    mClass.leaveScreen(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void enterScreen(SolverActivity act)
+    {
+    mClass.enterScreen(act);
+    }
+  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/solvers/ScreenSetupPosition.java b/src/main/java/org/distorted/solvers/ScreenSetupPosition.java
new file mode 100644
index 00000000..2430835f
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/ScreenSetupPosition.java
@@ -0,0 +1,845 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import static org.distorted.objectlib.metadata.ListObjects.*;
+import static org.distorted.objectlib.solvers.verifiers.SolverTablebase.*;
+
+import android.content.SharedPreferences;
+import android.content.res.Resources;
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Paint;
+import android.graphics.PorterDuff;
+import android.graphics.drawable.Drawable;
+import android.os.Bundle;
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import androidx.core.content.ContextCompat;
+
+import org.distorted.dialogs.DialogSolverError;
+import org.distorted.dialogs.DialogSolverImpossible;
+import org.distorted.dialogs.DialogSolvers;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.MainActivity;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objectlib.metadata.ListObjects;
+import org.distorted.objectlib.shape.*;
+import org.distorted.objectlib.solvers.verifiers.ResultScreen;
+import org.distorted.objectlib.solvers.verifiers.SolverAbstract;
+import org.distorted.objectlib.solvers.verifiers.SolvingList;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSetupPosition extends ScreenAbstract implements ResultScreen
+  {
+  private static final int RESET_DURATION = 1000;
+  private static final int MODE_NORMAL = 0;
+  private static final int MODE_DINO_4 = 1;
+
+  private static final int[][] colorsHex =
+    {
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
+    };
+  private static final int[][] colorsTet =
+    {
+      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+    };
+  private static final int[][] colorsOct =
+    {
+      {R.string.color_violet1,R.string.color_violet2,R.string.color_violet3,R.string.color_violet4,R.string.color_violet5,R.string.color_violet6,R.string.color_violet7 },
+      {R.string.color_grey1  ,R.string.color_grey2  ,R.string.color_grey3  ,R.string.color_grey4  ,R.string.color_grey5  ,R.string.color_grey6  ,R.string.color_grey7   },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
+      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
+      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+    };
+  private static final int[][] colorsDi4 =
+    {
+      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
+      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
+      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
+      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
+    };
+
+  private static Bitmap[] mBitmap;
+  private ImageButton[] mColorButton;
+  private TransparentImageButton mResetButton,mBackButton, mSolveButton;
+  private boolean mSolving;
+  private int mCurrentColor, mCurrentButton;
+  private int[] mFaceColors;
+  private int mColorMode;
+  private int mNumColors;
+  private int mNumBitmapRows;
+  private float mBitmapSize;
+  private WeakReference<SolverActivity> mWeakAct;
+  private int mObjectOrdinal;
+  private String[] mPhaseNames;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.unsetLock();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.setLock(false);
+
+    float width = act.getScreenWidthInPixels();
+    float heigh = act.getScreenHeightInPixels();
+
+    mWeakAct = new WeakReference<>(act);
+    mSolving = false;
+    mPhaseNames = null;
+
+    mObjectOrdinal = act.getObjectOrdinal();
+    control.solveOnly();
+    generateFaceColors(mObjectOrdinal);
+
+    mNumBitmapRows = mNumColors>8 ? 2 : 1;
+    mBitmapSize = computeBitmapSize(width,heigh);
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    if( mNumColors>0 )
+      {
+      setupBitmaps();
+      setupColorButtons(act);
+      markButton(act);
+      addButtonsToTopLayout(act,layoutTop);
+      }
+
+    // BOT ////////////////////////////
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+
+    LinearLayout layoutL = new LinearLayout(act);
+    layoutL.setLayoutParams(params);
+    LinearLayout layoutM = new LinearLayout(act);
+    layoutM.setLayoutParams(params);
+    LinearLayout layoutR = new LinearLayout(act);
+    layoutR.setLayoutParams(params);
+
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    setupResetButton(act);
+    setupSolveButton(act);
+    setupBackButton(act);
+
+    layoutL.addView(mResetButton);
+    layoutM.addView(mSolveButton);
+    layoutR.addView(mBackButton);
+
+    layoutBot.addView(layoutL);
+    layoutBot.addView(layoutM);
+    layoutBot.addView(layoutR);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private float computeBitmapSize(float width, float heigh)
+    {
+    final float BUTTON_RATIO = 0.75f;
+    float sizeV = (heigh/mNumBitmapRows)*MainActivity.RATIO_BAR;
+    float sizeH = (width*mNumBitmapRows)/mNumColors;
+
+    return BUTTON_RATIO*Math.min(sizeV,sizeH);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void addButtonsToTopLayout(SolverActivity act, LinearLayout layout)
+    {
+    if( mNumBitmapRows==1 )
+      {
+      for(ImageButton button: mColorButton) layout.addView(button);
+      }
+    else if( mNumBitmapRows==2 )
+      {
+      LinearLayout layoutV = new LinearLayout(act);
+      layoutV.setOrientation(LinearLayout.VERTICAL);
+      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
+      layoutV.setLayoutParams(params);
+      LinearLayout layoutT = new LinearLayout(act);
+      layoutT.setOrientation(LinearLayout.HORIZONTAL);
+      LinearLayout layoutB = new LinearLayout(act);
+      layoutB.setOrientation(LinearLayout.HORIZONTAL);
+
+      int numB = mColorButton.length;
+      for(int b=0     ; b<numB/2; b++) layoutT.addView(mColorButton[b]);
+      for(int b=numB/2; b<numB  ; b++) layoutB.addView(mColorButton[b]);
+
+      layoutV.addView(layoutT);
+      layoutV.addView(layoutB);
+      layout.addView(layoutV);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// This doesn't quite work in many cases, but in case of the solvers that will pop up in foreseeable
+// future it should be ok.
+
+  public void generateFaceColors(int object)
+    {
+    mColorMode = MODE_NORMAL;
+
+    if( object==PYRA_3.ordinal() ||
+        object==PYRA_4.ordinal() ||
+        object==PYRA_5.ordinal() ||
+        object==PDUO_2.ordinal() ||
+        object==JING_2.ordinal() ||
+        object==MORP_2.ordinal() ||
+        object==MORP_3.ordinal() ||
+        object==MORP_4.ordinal()  )
+      {
+      mNumColors  = ShapeTetrahedron.NUM_FACES;
+      mFaceColors = ShapeTetrahedron.FACE_COLORS;
+      }
+    else if( object==DIAM_2.ordinal() ||
+             object==DIAM_3.ordinal() ||
+             object==DIAM_4.ordinal() ||
+             object==TRAJ_3.ordinal() ||
+             object==TRAJ_4.ordinal() ||
+             object==PDIA_3.ordinal()  )
+      {
+      mNumColors  = ShapeOctahedron.NUM_FACES;
+      mFaceColors = ShapeOctahedron.FACE_COLORS;
+      }
+    else if( object==CRYS_3.ordinal() ||
+             object==STAR_3.ordinal() ||
+             object==PENT_2.ordinal() ||
+             object==KILO_3.ordinal() ||
+             object==KILO_5.ordinal() ||
+             object==MEGA_3.ordinal() ||
+             object==MEGA_5.ordinal()  )
+      {
+      mNumColors  = ShapeDodecahedron.NUM_FACES;
+      mFaceColors = ShapeDodecahedron.FACE_COLORS;
+      }
+    else if( object==BALL_4.ordinal() )
+      {
+      mNumColors  = ShapeDiamond.NUM_FACES;
+      mFaceColors = ShapeDiamond.FACE_COLORS;
+      }
+    else if( object==ICOS_2.ordinal() )
+      {
+      mNumColors  = ShapeIcosahedron.NUM_FACES;
+      mFaceColors = ShapeIcosahedron.FACE_COLORS;
+      }
+    else if( object==DIN4_3.ordinal() )
+      {
+      mNumColors  = 4;
+      mFaceColors = new int[] { ShapeColors.COLOR_YELLOW, ShapeColors.COLOR_RED, ShapeColors.COLOR_BLUE, ShapeColors.COLOR_WHITE};
+      mColorMode  = MODE_DINO_4;
+      }
+    else
+      {
+      mNumColors  = ShapeHexahedron.NUM_FACES;
+      mFaceColors = ShapeHexahedron.FACE_COLORS;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBitmaps()
+    {
+    final int SIZE = (int)mBitmapSize;
+    final float R = SIZE*0.15f;
+    final float M = SIZE*0.08f;
+
+    mBitmap = new Bitmap[mNumColors];
+
+    Paint paint = new Paint();
+    paint.setColor(0xff008800);
+    paint.setStyle(Paint.Style.FILL);
+
+    paint.setAntiAlias(true);
+    paint.setTextAlign(Paint.Align.CENTER);
+    paint.setStyle(Paint.Style.FILL);
+
+    for(int i=0; i<mNumColors; i++)
+      {
+      mBitmap[i] = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
+      Canvas canvas = new Canvas(mBitmap[i]);
+
+      paint.setColor(0xff000000);
+      canvas.drawRect(0, 0, SIZE, SIZE, paint);
+
+      paint.setColor(mFaceColors[i]);
+      canvas.drawRoundRect( M, M, SIZE-M, SIZE-M, R, R, paint);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int translateColor(int color)
+    {
+    if( mColorMode==MODE_DINO_4 )
+      {
+      int realColor = mFaceColors[color];
+      int[] hexColors = ShapeHexahedron.FACE_COLORS;
+
+      for(int i=0; i<6; i++)
+        if( hexColors[i]==realColor ) return i;
+      }
+
+    return color;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private boolean pressSolve(SolverActivity act)
+    {
+    int[] solverOrdinals = SolvingList.getSolverOrdinals(mObjectOrdinal);
+
+    if( solverOrdinals!=null  )
+      {
+      SolvingList slvList = SolvingList.getSolver(solverOrdinals[0]);
+      OperatingSystemInterface os = act.getInterface();
+      TwistyObject object = act.getObject();
+      SolverAbstract solver = slvList.create(os,object);
+
+      if( solver!=null )
+        {
+        int[] result = solver.validatePosition(object);
+
+        if( result[0]>=0 ) // position is valid
+          {
+          if( solverOrdinals.length==1 ) // just one solver - simply launch it
+            {
+            solver.solve(this,result);
+            return true;
+            }
+          else // more than one solver - pop up a choosing dialog
+            {
+            ListObjects objList = ListObjects.getObject(mObjectOrdinal);
+            String upperName = objList.name();
+            Bundle bundle = new Bundle();
+            bundle.putString("argument", upperName );
+            DialogSolvers solv = new DialogSolvers();
+            solv.setArguments(bundle);
+            solv.show( act.getSupportFragmentManager(), DialogSolvers.getDialogTag());
+            return false;
+            }
+          }
+        else
+          {
+          displayImpossibleDialog(result,solver.getFaceColors());
+          return false;
+          }
+        }
+      else
+        {
+        displayErrorDialog(act.getString(R.string.solver_generic_not_implemented));
+        return false;
+        }
+      }
+    else  // no solvers? Impossible!
+      {
+      displayErrorDialog("No solvers found for object "+mObjectOrdinal);
+      return false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void fail(String result) {}
+  public void setPhaseNames(String[] names) { mPhaseNames = names; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void startedSolving()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void stoppedSolving()
+    {
+
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupColorButtons(final SolverActivity act)
+    {
+    mColorButton = new ImageButton[mNumColors];
+
+    for(int i=0; i<mNumColors; i++)
+      {
+      final int ii = i;
+      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.MATCH_PARENT, 1.0f);
+
+      mColorButton[i] = new ImageButton(act);
+      mColorButton[i].setLayoutParams(params);
+      mColorButton[i].setImageBitmap(mBitmap[i]);
+
+      mColorButton[i].setOnClickListener( new View.OnClickListener()
+        {
+        @Override
+        public void onClick(View view)
+          {
+          mCurrentColor = translateColor(ii);
+          mCurrentButton= ii;
+          markButton(act);
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupResetButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
+    mResetButton = new TransparentImageButton(act, R.drawable.ui_reset, params);
+
+    mResetButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        control.resetTextureMapsEffect(RESET_DURATION);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupSolveButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mSolveButton = new TransparentImageButton(act,R.drawable.ui_solve,params);
+
+    mSolveButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        if( !mSolving && pressSolve(act) ) mSolving = true;
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        control.resetAllTextureMaps();
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void markButton(SolverActivity act)
+    {
+    if( mCurrentButton>=mNumColors )
+      {
+      mCurrentButton = 0;
+      mCurrentColor = translateColor(0);
+      }
+
+    for(int b=0; b<mNumColors; b++)
+      {
+      Drawable d = mColorButton[b].getBackground();
+      int s = b==mCurrentButton ? act.getSelectedColor() : act.getNormalColor();
+      d.setColorFilter(ContextCompat.getColor(act,s), PorterDuff.Mode.MULTIPLY);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor)
+    {
+    editor.putInt("stateSolver_color" , mCurrentColor );
+    editor.putInt("stateSolver_button", mCurrentButton);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void restorePreferences(SharedPreferences preferences)
+    {
+    mCurrentColor = preferences.getInt("stateSolver_color" , 0);
+    mCurrentButton= preferences.getInt("stateSolver_button", 0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getCurrentColor()
+    {
+    return mCurrentColor;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setSolved(final String moves)
+    {
+    mSolving = false;
+    final SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      act.runOnUiThread(new Runnable()
+        {
+        @Override
+        public void run()
+          {
+          ScreenList.switchScreen(act, ScreenList.SOLU);
+          ScreenSolutionSinglephased solution = (ScreenSolutionSinglephased) ScreenList.SOLU.getScreenClass();
+          solution.setSolution(moves);
+          if( !moves.isEmpty() ) act.doNotShowDialogAnymore();
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setSolved(final int[][] moves, final int phaseNumber, final int[][] subphases)
+    {
+    mSolving = false;
+    final SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      act.runOnUiThread(new Runnable()
+        {
+        @Override
+        public void run()
+          {
+          if( mPhaseNames!=null )
+            {
+            ScreenSolutionMultiphased screen = (ScreenSolutionMultiphased) ScreenList.PHAS.getScreenClass();
+            if( phaseNumber==0 )
+              {
+              ScreenList.switchScreen(act, ScreenList.PHAS);
+              screen.updateNames(mPhaseNames);
+              }
+            screen.setSolution(moves, phaseNumber,subphases);
+            }
+          else
+            {
+            ScreenList.switchScreen(act, ScreenList.SOLU);
+            ScreenSolutionSinglephased screen = (ScreenSolutionSinglephased) ScreenList.SOLU.getScreenClass();
+            screen.setSolution(moves);
+            }
+
+          if( moves!=null && moves.length>0 ) act.doNotShowDialogAnymore();
+          }
+        });
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void displayErrorDialog(String message)
+    {
+    mSolving = false;
+    SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      DialogSolverError dialog = new DialogSolverError();
+      Bundle bundle = new Bundle();
+      bundle.putString("argument", message );
+      dialog.setArguments(bundle);
+      dialog.show( act.getSupportFragmentManager(), null);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void displayImpossibleDialog(String message)
+    {
+    mSolving = false;
+    SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      DialogSolverImpossible dialog = new DialogSolverImpossible();
+      Bundle bundle = new Bundle();
+      bundle.putString("argument", message );
+      dialog.setArguments(bundle);
+      dialog.show( act.getSupportFragmentManager(), null);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void displayImpossibleDialog(int[] errorCode, int[] faceColors)
+    {
+    mSolving = false;
+    SolverActivity act = mWeakAct.get();
+
+    if( act!=null )
+      {
+      String message = error(act.getResources(),errorCode,faceColors);
+      displayImpossibleDialog(message);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  int getHexColor(int color,int variant) { return colorsHex[color][variant]; }
+  int getTetColor(int color,int variant) { return colorsTet[color][variant]; }
+  int getOctColor(int color,int variant) { return colorsOct[color][variant]; }
+  int getDi4Color(int color,int variant) { return colorsDi4[color][variant]; }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexCornerMissingError(Resources res, int face0, int face1, int face2)
+    {
+    int j0 = getHexColor(face0,3);
+    int j1 = getHexColor(face1,3);
+    int j2 = getHexColor(face2,4);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexCenterMissingError(Resources res, int face)
+    {
+    int color = getHexColor(face,2);
+    String clr= res.getString(color);
+    return res.getString(R.string.solver_generic_missing_center,clr);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeMissingError(Resources res, int face0, int face1)
+    {
+    int j0 = getHexColor(face0,3);
+    int j1 = getHexColor(face1,6);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeTwistedError(Resources res, int color0, int color1)
+    {
+    int j0 = getHexColor(color0,3);
+    int j1 = getHexColor(color1,6);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_twisted_edge,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexCornerTwistedError(Resources res, int color0, int color1, int color2)
+    {
+    int j0 = getHexColor(color0,3);
+    int j1 = getHexColor(color1,3);
+    int j2 = getHexColor(color2,5);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_twisted_corner,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeMonoError(Resources res, int color)
+    {
+    int j0 = getHexColor(color,3);
+    int j1 = getHexColor(color,6);
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_edge_mono,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String hexEdgeTwiceError(Resources res, int color0, int color1)
+    {
+    int j0 = getHexColor(color0,3);
+    int j1 = getHexColor(color1,6);
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_edge_twice,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String octCenterMissingError(Resources res, int face)
+    {
+    int index = getOctColor(face,2);
+    String color = res.getString(index);
+    return res.getString(R.string.solver_generic_missing_center,color);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String octCornerMissingError(Resources res, int f1, int f2)
+    {
+    int i1 = getOctColor(f1,3);
+    int i2 = getOctColor(f2,4);
+    String c1 = res.getString(i1);
+    String c2 = res.getString(i2);
+    return res.getString(R.string.solver_generic_missing_corner2,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetCornerMissingError(Resources res, int color0, int color1, int color2)
+    {
+    int j0 = getTetColor(color0,3);
+    int j1 = getTetColor(color1,3);
+    int j2 = getTetColor(color2,4);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetEdgeMissingError(Resources res, int face0, int face1)
+    {
+    int j0 = getTetColor(face0,3);
+    int j1 = getTetColor(face1,6);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+
+    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetCenterMissingError(Resources res, int face)
+    {
+    int j = getTetColor(face,2);
+    String c = res.getString(j);
+    return res.getString(R.string.solver_generic_missing_center,c);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String tetVertexMissingError(Resources res, int color0, int color1, int color2)
+    {
+    int j0 = getTetColor(color0,3);
+    int j1 = getTetColor(color1,3);
+    int j2 = getTetColor(color2,4);
+
+    String c0 = res.getString(j0);
+    String c1 = res.getString(j1);
+    String c2 = res.getString(j2);
+
+    return res.getString(R.string.solver_generic_missing_vertex,c0,c1,c2);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  String di4EdgeThreeError(Resources res, int color)
+    {
+    int j0 = getDi4Color(color,7);
+    String c0 = res.getString(j0);
+    return res.getString(R.string.solver_generic_edge_three,c0);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public String error(Resources res, int[] err, int[] faceColors)
+    {
+    switch(err[0])
+      {
+      case ERROR_HEX_CORNER_MISSING    : return hexCornerMissingError(res,err[1],err[2],err[3]);
+      case ERROR_HEX_CENTER_MISSING    : return hexCenterMissingError(res,err[1]);
+      case ERROR_HEX_EDGE_MISSING      : return hexEdgeMissingError(res,err[1],err[2]);
+      case ERROR_HEX_EDGE_TWISTED      : return hexEdgeTwistedError(res,err[1],err[2]);
+      case ERROR_HEX_EDGE_MONOCHROMATIC: return hexEdgeMonoError(res,err[1]);
+      case ERROR_HEX_EDGE_TWICE        : return hexEdgeTwiceError(res,err[1],err[2]);
+      case ERROR_HEX_CORNER_TWISTED    : return hexCornerTwistedError(res,err[1],err[2],err[3]);
+
+      case ERROR_TET_CORNER_MISSING    : return tetCornerMissingError(res,err[1],err[2],err[3]);
+      case ERROR_TET_VERTEX_MISSING    : return tetVertexMissingError(res,err[1],err[2],err[3]);
+      case ERROR_TET_EDGE_MISSING      : return tetEdgeMissingError(res,faceColors[err[1]],faceColors[err[2]]);
+      case ERROR_TET_CENTER_MISSING    : return tetCenterMissingError(res,err[1]);
+
+      case ERROR_OCT_CENTER_MISSING    : return octCenterMissingError(res,err[1]);
+      case ERROR_OCT_CORNER_MISSING    : return octCornerMissingError(res,err[1],err[2]);
+
+      case ERROR_DI4_EDGE_THREE        : return di4EdgeThreeError(res,err[1]);
+
+      case ERROR_CORNERS_CANNOT        : return res.getString(R.string.solver_generic_corners_cannot);
+      case ERROR_EDGE_CANNOT           : return res.getString(R.string.solver_generic_edges_cannot);
+      case ERROR_CORNER_TWISTED        : return res.getString(R.string.solver_generic_corner_twist);
+      case ERROR_CORNER_TWIST_90       : return res.getString(R.string.solver_generic_corner_twist) + " (90)";
+      case ERROR_CORNER_TWIST_180      : return res.getString(R.string.solver_generic_corner_twist) + " (180)";
+      case ERROR_EDGE_TWISTED          : return res.getString(R.string.solver_generic_edge_twist);
+      case ERROR_TWO_CENTERS           : return res.getString(R.string.solver_generic_two_centers);
+      case ERROR_TWO_CORNERS           : return res.getString(R.string.solver_generic_two_corners);
+      case ERROR_TWO_EDGES             : return res.getString(R.string.solver_generic_two_edges);
+      case ERROR_FREE_CORNERS_NOT_EVEN : return res.getString(R.string.solver_generic_free_corners_odd);
+      case ERROR_FREE_CORNERS_ROTATED  : return res.getString(R.string.solver_generic_free_corners_rotated);
+      case ERROR_VERTICES_CANNOT       : return res.getString(R.string.solver_generic_vertices_cannot);
+      case ERROR_C_V_DONT_MATCH        : return res.getString(R.string.solver_generic_c_v_dont_match);
+      case ERROR_TWO_CORNERS_TWO_EDGES : return res.getString(R.string.solver_two_corners_two_edges);
+      }
+
+    return null;
+    }
+  }
diff --git a/src/main/java/org/distorted/solvers/ScreenSolutionMultiphased.java b/src/main/java/org/distorted/solvers/ScreenSolutionMultiphased.java
new file mode 100644
index 00000000..b365e4f6
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/ScreenSolutionMultiphased.java
@@ -0,0 +1,578 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2024 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.library.effect.PostprocessEffectGlow;
+import org.distorted.library.main.DistortedEffects;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.library.message.EffectListener;
+import org.distorted.library.type.Dynamic2D;
+import org.distorted.library.type.Dynamic4D;
+import org.distorted.library.type.Static2D;
+import org.distorted.library.type.Static4D;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.MovesFinished;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSolutionMultiphased extends ScreenAbstract implements MovesFinished, EffectListener
+  {
+  private static final int MOVES_PLACE_0 = 100;
+  private static final int MOVES_PLACE_1 = 101;
+  private static final int FLASH_TIME = 1200;
+  private static final int MILLIS_PER_DEGREE = 6;
+
+  private WeakReference<SolverActivity> mAct;
+  private TransparentImageButton mPrevButton, mNextButton, mBackButton, mPrevPhase, mNextPhase;
+  private float mButtonSize;
+
+  private TextView mMovesText, mMovesPhase;
+  private String[] mPhaseNames;
+  private int mNumPhases;
+  private int[][][] mMoves;
+  private int[][] mCubitsNotInvolved;
+  private int mNumMoves,mCurrMove,mCurrPhase;
+  private boolean mCanMove;
+
+  private Dynamic2D mHaloAndRadiusDyn;
+  private Dynamic4D mColorDyn;
+  private PostprocessEffectGlow mGlow;
+  private boolean mEffectWorking;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.solveOnly();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final SolverActivity act)
+    {
+    mAct = new WeakReference<>(act);
+
+    mHaloAndRadiusDyn = new Dynamic2D(FLASH_TIME,1.0f);
+    mHaloAndRadiusDyn.add(new Static2D( 0,0));
+    mHaloAndRadiusDyn.add(new Static2D(10,5));
+
+    mColorDyn = new Dynamic4D(FLASH_TIME,1.0f);
+
+    final int[] colors  = new int[] {1,1,1}; // white
+
+    Static4D P1 = new Static4D(colors[0],colors[1],colors[2], 0.0f);
+    Static4D P2 = new Static4D(colors[0],colors[1],colors[2], 1.0f);
+    mColorDyn.add(P1);
+    mColorDyn.add(P2);
+
+    mGlow = new PostprocessEffectGlow(mHaloAndRadiusDyn,mColorDyn);
+
+    float width = act.getScreenWidthInPixels();
+    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    setupPrevPhase(act);
+    setupNextPhase(act);
+    setupTextPhase(act,width);
+
+    layoutTop.addView(mPrevPhase);
+    layoutTop.addView(mMovesPhase);
+    layoutTop.addView(mNextPhase);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutMid = new LinearLayout(act);
+    layoutMid.setLayoutParams(paramsM);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width);
+
+    layoutLeft.addView(mPrevButton);
+    layoutLeft.addView(mMovesText);
+    layoutLeft.addView(mNextButton);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutMid);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { backMove(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { nextMove(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final SolverActivity act, final float width)
+    {
+    int padding = (int)(width*SolverActivity.PADDING);
+    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesText = new TextView(act);
+    mMovesText.setTextSize(20);
+    mMovesText.setLayoutParams(params);
+    mMovesText.setPadding(padding,0,padding,0);
+    mMovesText.setGravity(Gravity.CENTER);
+    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevPhase(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevPhase = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevPhase.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { backPhase(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextPhase(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextPhase = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextPhase.setOnClickListener( new View.OnClickListener()
+      {
+      @Override public void onClick(View v) { nextPhase(); }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextPhase(final SolverActivity act, final float width)
+    {
+    int padding = (int)(width*SolverActivity.PADDING);
+    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesPhase = new TextView(act);
+    mMovesPhase.setTextSize(20);
+    mMovesPhase.setLayoutParams(params);
+    mMovesPhase.setPadding(padding,0,padding,0);
+    mMovesPhase.setGravity(Gravity.CENTER);
+    mMovesPhase.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesPhase.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor) { }
+  public void restorePreferences(SharedPreferences preferences) { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void backPhase()
+    {
+    SolverActivity act = mAct.get();
+    ObjectControl control = act.getControl();
+
+    if( mCurrMove>0 )
+      {
+      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mCurrMove, false);
+      control.applyScrambles(moves);
+      mCurrMove = 0;
+      }
+    else if( mCurrPhase>0 )
+      {
+      mCurrPhase--;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = 0;
+      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mNumMoves, false);
+      control.applyScrambles(moves);
+      }
+    else
+      {
+      mCurrPhase = mNumPhases-1;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = mNumMoves;
+      int[][] moves = transformMoves(mMoves, true);
+      control.applyScrambles(moves);
+      }
+
+    setText(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void nextPhase()
+    {
+    SolverActivity act = mAct.get();
+    ObjectControl control = act.getControl();
+
+    if( mCurrPhase<mNumPhases-1 )
+      {
+      glowCubits(mCubitsNotInvolved[mCurrPhase]);
+      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
+      control.applyScrambles(moves);
+      mCurrPhase++;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = 0;
+      }
+    else if( mCurrMove<mNumMoves )
+      {
+      glowCubits(mCubitsNotInvolved[mCurrPhase]);
+      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
+      control.applyScrambles(moves);
+      mCurrMove = mNumMoves;
+      }
+    else
+      {
+      mCurrPhase = 0;
+      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+      mCurrMove = 0;
+      int[][] moves = transformMoves(mMoves, false);
+      control.applyScrambles(moves);
+      }
+
+    setText(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setText(SolverActivity act)
+    {
+    int currMove = 0;
+    int totalMove = 0;
+
+    if( mMoves!=null )
+      {
+      currMove = mCurrMove;
+      for(int p=0; p<mCurrPhase; p++) currMove  += (mMoves[p]==null ? 0: mMoves[p].length);
+      for(int p=0; p<mNumPhases; p++) totalMove += (mMoves[p]==null ? 0: mMoves[p].length);
+      }
+
+    final int cMove = currMove;
+    final int tMove = totalMove;
+
+    act.runOnUiThread(new Runnable()
+      {
+      @Override
+      public void run()
+        {
+        mMovesPhase.setText(mPhaseNames[mCurrPhase]+" "+mCurrMove+"/"+mNumMoves);
+        mMovesText.setText(cMove+"/"+tMove);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int[][] transformMoves(int[][] moves, int start, int end, boolean front)
+    {
+    int mult = front ? 1:-1;
+    int len = end-start;
+    int[][] ret = new int[len][];
+
+    for(int m=0; m<len; m++)
+      {
+      int[] mv = moves[front ? start+m : end-1-m];
+      int[] rt = new int[3];
+      rt[0] = mv[0];
+      rt[1] = (1<<mv[1]);
+      rt[2] = mult*mv[2];
+      ret[m] = rt;
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int[][] transformMoves(int[][][] moves, boolean front)
+    {
+    int len = moves.length;
+    int totalLen = 0;
+    for (int[][] move : moves) totalLen += (move==null ? 0 : move.length);
+
+    int[][] ret = new int[totalLen][];
+    int mult = front ? 1:-1;
+    int index = 0;
+
+    for(int m=0; m<len; m++)
+      {
+      int[][] mv = moves[front ? m : len-1-m];
+      int l = (mv==null ? 0 : mv.length);
+
+      for(int p=0; p<l; p++)
+        {
+        int[] mve = mv[front ? p : l-1-p];
+        int[] rt = new int[3];
+        rt[0] = mve[0];
+        rt[1] = (1<<mve[1]);
+        rt[2] = mult*mve[2];
+        ret[index++] = rt;
+        }
+      }
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void glowCubits(int[] cubits)
+    {
+    if( !mEffectWorking )
+      {
+      mEffectWorking = true;
+      SolverActivity act=mAct.get();
+      ObjectControl control = act.getControl();
+      TwistyObject object=control.getObject();
+      DistortedEffects effects=object.getObjectEffects();
+      effects.apply(mGlow);
+
+      MeshBase mesh=object.getObjectMesh();
+      mesh.setComponentsNotAffectedByPostprocessing(cubits);
+
+      mHaloAndRadiusDyn.resetToBeginning();
+      mColorDyn.resetToBeginning();
+      mGlow.notifyWhenFinished(this);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private int[] computeCubitsNotInvolved(int[][] subphases, int numCubits)
+    {
+    int numCubitsInvolved = 0;
+    boolean[] involved = new boolean[numCubits];
+
+    for(int[] sub : subphases)
+      if( sub!=null )
+        for(int s : sub)
+          {
+          numCubitsInvolved++;
+          involved[s] = true;
+          }
+
+    int[] ret = new int[numCubits-numCubitsInvolved];
+    int index = 0;
+
+    for(int c=0; c<numCubits; c++)
+      if( !involved[c] ) ret[index++] = c;
+
+    return ret;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void backMove()
+    {
+    if( mMoves!=null && mCanMove )
+      {
+      SolverActivity act=mAct.get();
+
+      if( mCurrMove>0 )
+        {
+        mCanMove = false;
+        int[] move = mMoves[mCurrPhase][--mCurrMove];
+        ObjectControl control = act.getControl();
+        control.blockTouch(MOVES_PLACE_0);
+        control.addRotation(this, move[0], (1<<move[1]), -move[2], MILLIS_PER_DEGREE);
+        }
+      else if( mCurrPhase>0 )
+        {
+        mCurrPhase--;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = mNumMoves;
+        glowCubits(mCubitsNotInvolved[mCurrPhase]);
+        }
+      else
+        {
+        mCurrPhase = mNumPhases-1;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = mNumMoves;
+        int[][] moves = transformMoves(mMoves, true);
+        ObjectControl control = act.getControl();
+        control.applyScrambles(moves);
+        glowCubits(mCubitsNotInvolved[mCurrPhase]);
+        }
+
+      setText(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void nextMove()
+    {
+    if( mMoves!=null && mCanMove )
+      {
+      SolverActivity act=mAct.get();
+
+      if( mCurrMove<mNumMoves )
+        {
+        mCanMove = false;
+        int[] move = mMoves[mCurrPhase][mCurrMove++];
+        ObjectControl control = act.getControl();
+        control.blockTouch(MOVES_PLACE_1);
+        control.addRotation(this, move[0], (1<<move[1]), move[2], MILLIS_PER_DEGREE);
+        if( mCurrMove==mNumMoves && mCurrPhase==mNumPhases-1 ) glowCubits(mCubitsNotInvolved[mCurrPhase]);
+        }
+      else if( mCurrPhase<mNumPhases-1 )
+        {
+        mCurrPhase++;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = 0;
+        glowCubits(mCubitsNotInvolved[mCurrPhase-1]);
+        }
+      else
+        {
+        mCurrPhase = 0;
+        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
+        mCurrMove = 0;
+        int[][] moves = transformMoves(mMoves, false);
+        ObjectControl control = act.getControl();
+        control.applyScrambles(moves);
+        }
+
+      setText(act);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void updateNames(String[] names)
+    {
+    mPhaseNames = names;
+    mNumPhases = names.length;
+    mMoves = new int[mNumPhases][][];
+    mCubitsNotInvolved = new int[mNumPhases][];
+    mCanMove = true;
+    if( mAct!=null ) setSolution(null,0,null);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setSolution(int[][] moves, int phase, int[][] subphases)
+    {
+    SolverActivity act=mAct.get();
+
+    if( subphases!=null )
+      {
+      ObjectControl control=act.getControl();
+      TwistyObject object=control.getObject();
+      int numCubits=object.getNumCubits();
+      mCubitsNotInvolved[phase]= computeCubitsNotInvolved(subphases, numCubits);
+      }
+
+    mMoves[phase] = moves;
+    if( phase==0 ) mNumMoves = (moves==null ? 0 : moves.length);
+    mCurrPhase = 0;
+    mCurrMove = 0;
+
+    setText(act);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onActionFinished(final long effectID)
+    {
+    mCanMove = true;
+    SolverActivity act=mAct.get();
+    ObjectControl control = act.getControl();
+    control.unblockRotation();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void effectFinished(long id)
+    {
+    SolverActivity act=mAct.get();
+    ObjectControl control = act.getControl();
+    TwistyObject object=control.getObject();
+    DistortedEffects effects=object.getObjectEffects();
+    effects.abortById(id);
+    mEffectWorking = false;
+    }
+  }
diff --git a/src/main/java/org/distorted/solvers/ScreenSolutionSinglephased.java b/src/main/java/org/distorted/solvers/ScreenSolutionSinglephased.java
new file mode 100644
index 00000000..a2f700fd
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/ScreenSolutionSinglephased.java
@@ -0,0 +1,284 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import android.content.SharedPreferences;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.LayoutInflater;
+import android.view.View;
+import android.widget.LinearLayout;
+import android.widget.TextView;
+
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.MovesFinished;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.patterns.RubikPattern;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class ScreenSolutionSinglephased extends ScreenAbstract implements MovesFinished
+  {
+  private static final int MILLIS_PER_DEGREE = 6;
+
+  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
+  private TextView mMovesText;
+  private int[][] mMoves;
+  private int mCurrMove, mNumMoves;
+  private boolean mCanRotate;
+  private float mButtonSize;
+  private WeakReference<SolverActivity> mAct;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void leaveScreen(SolverActivity act)
+    {
+    ObjectControl control = act.getControl();
+    control.solveOnly();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void enterScreen(final SolverActivity act)
+    {
+    mAct = new WeakReference<>(act);
+
+    float width = act.getScreenWidthInPixels();
+    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
+    float titleSize  = width*SolverActivity.TITLE_TEXT_SIZE;
+
+    LayoutInflater inflater = act.getLayoutInflater();
+
+    // TOP ////////////////////////////
+    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
+    layoutTop.removeAllViews();
+
+    final TextView text = (TextView)inflater.inflate(R.layout.upper_text, null);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
+    text.setText(R.string.solution);
+    layoutTop.addView(text);
+
+    // BOT ////////////////////////////
+    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
+    layoutBot.removeAllViews();
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutMid = new LinearLayout(act);
+    layoutMid.setLayoutParams(paramsM);
+    LinearLayout layoutRight = new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width);
+
+    layoutLeft.addView(mPrevButton);
+    layoutLeft.addView(mMovesText);
+    layoutLeft.addView(mNextButton);
+
+    setupBackButton(act);
+
+    layoutRight.addView(mBackButton);
+
+    layoutBot.addView(layoutLeft);
+    layoutBot.addView(layoutMid);
+    layoutBot.addView(layoutRight);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        backMove(control);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
+
+    mNextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        makeMove(control);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final SolverActivity act, final float width)
+    {
+    int padding = (int)(width*SolverActivity.PADDING);
+    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesText = new TextView(act);
+    mMovesText.setTextSize(20);
+    mMovesText.setLayoutParams(params);
+    mMovesText.setPadding(padding,0,padding,0);
+    mMovesText.setGravity(Gravity.CENTER);
+    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final SolverActivity act)
+    {
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ScreenList.goBack(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void makeMove(ObjectControl control)
+    {
+    if( mCanRotate )
+      {
+      mCurrMove++;
+
+      if( mCurrMove>mNumMoves )
+        {
+        mCurrMove= 0;
+        control.initializeObject(null);
+        }
+      else
+        {
+        int axis      = mMoves[mCurrMove-1][0];
+		int rowBitmap = mMoves[mCurrMove-1][1];
+		int bareAngle = mMoves[mCurrMove-1][2];
+
+        if( bareAngle!=0 )
+          {
+          mCanRotate = false;
+          control.addRotation(this, axis, rowBitmap, bareAngle, MILLIS_PER_DEGREE);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: solution contains angle 0");
+          }
+        }
+      }
+    else
+      {
+      android.util.Log.e("solution", "failed to make move!");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void backMove(ObjectControl control)
+    {
+    if( mCanRotate )
+      {
+      mCurrMove--;
+
+      if( mCurrMove<0 )
+        {
+        mCurrMove=mNumMoves;
+        control.initializeObject(mMoves);
+        }
+      else
+        {
+        int axis      = mMoves[mCurrMove][0];
+		int rowBitmap = mMoves[mCurrMove][1];
+		int bareAngle = mMoves[mCurrMove][2];
+
+        if( bareAngle!=0 )
+          {
+          mCanRotate = false;
+          control.addRotation(this, axis, rowBitmap, -bareAngle, MILLIS_PER_DEGREE);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: solution contains angle 0");
+          }
+        }
+      }
+    else
+      {
+      android.util.Log.e("solution", "failed to back move!");
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setSolution(String moves)
+    {
+    mCanRotate= true;
+    mCurrMove = 0;
+    mNumMoves = moves.length()/4;
+    mMoves    = new int[mNumMoves][3];
+
+    RubikPattern.parseMoves(mMoves,mNumMoves,moves);
+
+    SolverActivity act = mAct.get();
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void setSolution(int[][] moves)
+    {
+    mCanRotate= true;
+    mCurrMove = 0;
+    mNumMoves = moves==null ? 0 : moves.length;
+    mMoves    = moves;
+
+    SolverActivity act = mAct.get();
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void savePreferences(SharedPreferences.Editor editor) { }
+  public void restorePreferences(SharedPreferences preferences) { }
+  public void onActionFinished(final long effectID) { mCanRotate = true; }
+  }
diff --git a/src/main/java/org/distorted/solvers/SolverActivity.java b/src/main/java/org/distorted/solvers/SolverActivity.java
new file mode 100644
index 00000000..ec869d00
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/SolverActivity.java
@@ -0,0 +1,239 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import android.content.SharedPreferences;
+import android.os.Build;
+import android.os.Bundle;
+import android.view.DisplayCutout;
+import android.view.ViewGroup;
+import android.widget.LinearLayout;
+
+import org.distorted.dialogs.DialogError;
+import org.distorted.dialogs.DialogMessage;
+import org.distorted.helpers.BaseActivity;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.main.R;
+import org.distorted.objectlib.helpers.OperatingSystemInterface;
+import org.distorted.objectlib.main.InitAssets;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.os.OSInterface;
+
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class SolverActivity extends BaseActivity
+{
+    public static final float RATIO_UPP       = 0.100f;
+    public static final float PADDING         = 0.010f;
+    public static final float SMALL_MARGIN    = 0.004f;
+    public static final float BUTTON_TEXT_SIZE= 0.050f;
+    public static final float TITLE_TEXT_SIZE = 0.060f;
+
+    private static final int ACTIVITY_NUMBER = 4;
+    private static final float RATIO_INSET= 0.09f;
+
+    private int mObjectOrdinal;
+    private boolean mDisplayMessageDialog;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle savedState)
+      {
+      super.onCreate(savedState);
+      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
+      setContentView(R.layout.solver);
+
+      Bundle b = getIntent().getExtras();
+      mObjectOrdinal = b!=null ? b.getInt("obj") : 0;
+      mDisplayMessageDialog = true;
+
+      computeScreenDimensions();
+      hideNavigationBar();
+      cutoutHack();
+      computeUpperBarHeight(RATIO_UPP);
+      computeLowerBarHeight(RATIO_BAR);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onAttachedToWindow()
+      {
+      super.onAttachedToWindow();
+
+      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
+        {
+        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
+        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
+
+        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
+        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
+        paramsHid.height = (int)(insetHeight*RATIO_INSET);
+        layoutHid.setLayoutParams(paramsHid);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      super.onPause();
+      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
+      view.onPause();
+      DistortedLibrary.onPause(ACTIVITY_NUMBER);
+      savePreferences();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      DistortedLibrary.onResume(ACTIVITY_NUMBER);
+      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
+      view.onResume();
+
+      createObject();
+
+      restorePreferences();
+      ScreenList.setScreen(this);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
+      super.onDestroy();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void savePreferences()
+      {
+      SharedPreferences.Editor editor = mPreferences.edit();
+
+      for( int i=0; i< ScreenList.LENGTH; i++ )
+        {
+        ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
+        }
+
+      ScreenList.savePreferences(editor);
+
+      editor.putBoolean("solverDisplayDialog", mDisplayMessageDialog );
+
+      boolean success = editor.commit();
+      if( !success ) android.util.Log.e("D", "Failed to save preferences");
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void restorePreferences()
+      {
+      for( int i=0; i<ScreenList.LENGTH; i++ )
+        {
+        ScreenList.getScreen(i).getScreenClass().restorePreferences(mPreferences);
+        }
+
+      ScreenList.restorePreferences(mPreferences);
+
+      mDisplayMessageDialog = mPreferences.getBoolean("solverDisplayDialog",true);
+
+      if( mDisplayMessageDialog )
+        {
+        Bundle bundle = new Bundle();
+        bundle.putString("argument", getString(R.string.solver_message) );
+        DialogMessage diag = new DialogMessage();
+        diag.setArguments(bundle);
+        diag.show( getSupportFragmentManager(), DialogMessage.getDialogTag());
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void doNotShowDialogAnymore()
+      {
+      mDisplayMessageDialog = false;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void OpenGLError()
+      {
+      DialogError errDiag = new DialogError();
+      errDiag.show(getSupportFragmentManager(), null);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyObject getObject()
+      {
+      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
+      return view.getObjectControl().getObject();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public DistortedScreen getScreen()
+      {
+      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
+      return view.getRenderer().getScreen();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public ObjectControl getControl()
+      {
+      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
+      return view.getObjectControl();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getObjectOrdinal()
+      {
+      return mObjectOrdinal;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void createObject()
+      {
+      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
+      ObjectControl control = view.getObjectControl();
+      RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+      InputStream jsonStream = object==null ? null : object.getObjectStream(this);
+      InputStream meshStream = object==null ? null : object.getMeshStream(this);
+      String name = object==null ? "NULL" : object.getUpperName();
+      OSInterface os = view.getInterface();
+      InitAssets asset = new InitAssets(jsonStream,meshStream,os);
+      control.changeIfDifferent(mObjectOrdinal,name,TwistyObject.MODE_NORM,asset);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public OperatingSystemInterface getInterface()
+      {
+      SolverSurfaceView view  = findViewById(R.id.solverSurfaceView);
+      return view.getInterface();
+      }
+}
diff --git a/src/main/java/org/distorted/solvers/SolverObjectLibInterface.java b/src/main/java/org/distorted/solvers/SolverObjectLibInterface.java
new file mode 100644
index 00000000..a56ed208
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/SolverObjectLibInterface.java
@@ -0,0 +1,194 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.message.EffectMessageSender;
+import org.distorted.main.BuildConfig;
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.ObjectLibInterface;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+import org.distorted.objectlib.solvers.verifiers.SolvingList;
+
+import java.lang.ref.WeakReference;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class SolverObjectLibInterface implements ObjectLibInterface
+{
+  private final WeakReference<SolverActivity> mAct;
+  private int mLastCubitColor, mLastCubit, mLastCubitFace;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  SolverObjectLibInterface(SolverActivity act)
+    {
+    mAct = new WeakReference<>(act);
+    mLastCubitColor = -1;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onScrambleEffectFinished() { }
+  public void onRemoveRotation(int axis, int row, int angle) { }
+  public void onBeginRotation() { }
+  public void failedToDrag() { }
+  public void onSolved() { }
+  public void onObjectCreated(long time) { }
+  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportProblem(String problem, boolean recordException)
+    {
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("libInterface", problem);
+      }
+    else
+      {
+      if( recordException )
+        {
+        Exception ex = new Exception(problem);
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.setCustomKey("problem" , problem);
+        crashlytics.recordException(ex);
+        }
+      else
+        {
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.log(problem);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportScramblingProblem(int place, long pause, long resume, long time)
+    {
+    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportRotationProblem(int place, long pause, long resume, long time)
+    {
+    String error = "ROTATION BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume);
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportThreadProblem(int place, long pause, long resume, long time)
+    {
+    String error = EffectMessageSender.reportState();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause  );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
+    {
+    switch(type)
+      {
+      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportJSONError(String error, int ordinal)
+    {
+    RubikObject object = RubikObjectList.getObject(ordinal);
+    String name = object==null ? "NULL" : object.getUpperName();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("name" , name );
+      crashlytics.setCustomKey("JSONerror", error );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onReplaceModeDown(int cubit, int face)
+    {
+    SolverActivity act = mAct.get();
+    ScreenSetupPosition solver = (ScreenSetupPosition) ScreenList.SVER.getScreenClass();
+    int color = solver.getCurrentColor();
+    int currObject = act.getObjectOrdinal();
+    mLastCubitColor = SolvingList.cubitIsLocked(currObject,cubit);
+    mLastCubit = cubit;
+    mLastCubitFace = face;
+    ObjectControl control = act.getControl();
+    control.setTextureMap( cubit, face, color );
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onReplaceModeUp()
+    {
+    if( mLastCubitColor>=0 )
+      {
+      ObjectControl control = mAct.get().getControl();
+      control.setTextureMap( mLastCubit, mLastCubitFace, mLastCubitColor );
+      mLastCubitColor = -1;
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/solvers/SolverRenderer.java b/src/main/java/org/distorted/solvers/SolverRenderer.java
new file mode 100644
index 00000000..16f8bdf0
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/SolverRenderer.java
@@ -0,0 +1,159 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import android.content.res.Resources;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.library.main.InternalOutputSurface;
+import org.distorted.library.mesh.MeshBase;
+import org.distorted.main.BuildConfig;
+import org.distorted.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectControl;
+
+import java.io.InputStream;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class SolverRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
+{
+   private final SolverSurfaceView mView;
+   private final Resources mResources;
+   private final DistortedScreen mScreen;
+   private final ObjectControl mControl;
+   private boolean mErrorShown;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   SolverRenderer(SolverSurfaceView v)
+     {
+     mView = v;
+     mResources = v.getResources();
+
+     mErrorShown = false;
+     mControl = v.getObjectControl();
+     mScreen = new DistortedScreen();
+
+     SolverActivity act = (SolverActivity)v.getContext();
+     act.setUpBackgroundColor(mScreen);
+
+     mScreen.enableDepthStencil(InternalOutputSurface.DEPTH_NO_STENCIL);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedScreen getScreen()
+     {
+     return mScreen;
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// various things are done here delayed, 'after the next render' as not to be done mid-render and
+// cause artifacts.
+
+   @Override
+   public void onDrawFrame(GL10 glUnused)
+     {
+     long time = System.currentTimeMillis();
+     mControl.preRender();
+     mScreen.render(time);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceChanged(GL10 glUnused, int width, int height)
+      {
+      mScreen.resize(width,height);
+      mView.setScreenSize(width,height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
+      {
+      DistortedLibrary.setMax(EffectType.VERTEX,ObjectControl.MAX_QUATS+1);
+      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
+
+      VertexEffectRotate.enable();
+      VertexEffectQuaternion.enable();
+      BaseEffect.Type.enableEffects();
+
+      DistortedLibrary.onSurfaceCreated(this,1);
+      DistortedLibrary.setCull(true);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void distortedException(Exception ex)
+     {
+     String message = ex.getMessage();
+     String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+     String version = GLES30.glGetString(GLES30.GL_VERSION);
+     String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+     String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+     if( message==null ) message = "exception NULL";
+
+     if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("DISTORTED", message );
+       android.util.Log.e("DISTORTED", "GLSL Version "+shading);
+       android.util.Log.e("DISTORTED", "GL Version "  +version);
+       android.util.Log.e("DISTORTED", "GL Vendor "   +vendor);
+       android.util.Log.e("DISTORTED", "GL Renderer " +renderer);
+       }
+     else
+       {
+       FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+       crashlytics.setCustomKey("DistortedError", message );
+       crashlytics.setCustomKey("GLSL Version"  , shading );
+       crashlytics.setCustomKey("GLversion"     , version );
+       crashlytics.setCustomKey("GL Vendor "    , vendor  );
+       crashlytics.setCustomKey("GLSLrenderer"  , renderer);
+       crashlytics.recordException(ex);
+       }
+
+     int glsl = DistortedLibrary.getGLSL();
+
+     if( glsl< 300 && !mErrorShown )
+       {
+       mErrorShown = true;
+       SolverActivity act = (SolverActivity)mView.getContext();
+       act.OpenGLError();
+       }
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public InputStream localFile(int fileID)
+     {
+     return mResources.openRawResource(fileID);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void logMessage(String message)
+     {
+     android.util.Log.e("Rubik", message );
+     }
+}
diff --git a/src/main/java/org/distorted/solvers/SolverSurfaceView.java b/src/main/java/org/distorted/solvers/SolverSurfaceView.java
new file mode 100644
index 00000000..f00083ef
--- /dev/null
+++ b/src/main/java/org/distorted/solvers/SolverSurfaceView.java
@@ -0,0 +1,149 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2023 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.solvers;
+
+import android.annotation.SuppressLint;
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+import android.view.MotionEvent;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObjectNode;
+import org.distorted.os.OSInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class SolverSurfaceView extends GLSurfaceView
+{
+    private ObjectControl mObjectController;
+    private OSInterface mInterface;
+    private SolverRenderer mRenderer;
+    private boolean mCreated;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)));
+
+      if( !mCreated )
+        {
+        mCreated = true;
+        mObjectController.createNode(width,height);
+        TwistyObjectNode objectNode = mObjectController.getNode();
+        objectNode.glDepthMask(false);
+        objectNode.glStencilMask(0);
+        mRenderer.getScreen().attach(objectNode);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    SolverRenderer getRenderer()
+      {
+      return mRenderer;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    OSInterface getInterface()
+      {
+      return mInterface;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    ObjectControl getObjectControl()
+      {
+      return mObjectController;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public SolverSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      mCreated = false;
+
+      if(!isInEditMode())
+        {
+        SolverActivity act = (SolverActivity)context;
+        SolverObjectLibInterface ref = new SolverObjectLibInterface(act);
+        mInterface = new OSInterface(act,ref);
+        mObjectController = new ObjectControl(mInterface);
+        mRenderer = new SolverRenderer(this);
+
+        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        try
+          {
+          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+          int esVersion = configurationInfo.reqGlEsVersion>>16;
+          setEGLContextClientVersion(esVersion);
+          setRenderer(mRenderer);
+          }
+        catch(Exception ex)
+          {
+          act.OpenGLError();
+
+          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+          String version = GLES30.glGetString(GLES30.GL_VERSION);
+          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("GLSL Version"  , shading );
+          crashlytics.setCustomKey("GL version"    , version );
+          crashlytics.setCustomKey("GL Vendor "    , vendor  );
+          crashlytics.setCustomKey("GLSL renderer" , renderer);
+          crashlytics.recordException(ex);
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onPause()
+      {
+      super.onPause();
+      mObjectController.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onResume()
+      {
+      super.onResume();
+      mObjectController.onResume();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @SuppressLint("ClickableViewAccessibility")
+    @Override
+    public boolean onTouchEvent(MotionEvent event)
+      {
+      mInterface.setMotionEvent(event);
+      int mode = ScreenList.getMode();
+      return mObjectController.onTouchEvent(mode);
+      }
+}
+
diff --git a/src/main/java/org/distorted/solverui/ScreenAbstract.java b/src/main/java/org/distorted/solverui/ScreenAbstract.java
deleted file mode 100644
index d4bf5266..00000000
--- a/src/main/java/org/distorted/solverui/ScreenAbstract.java
+++ /dev/null
@@ -1,22 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.SharedPreferences;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public abstract class ScreenAbstract
-  {
-  abstract void enterScreen(SolverActivity act);
-  abstract void leaveScreen(SolverActivity act);
-  public abstract void savePreferences(SharedPreferences.Editor editor);
-  public abstract void restorePreferences(SharedPreferences preferences);
-  }
diff --git a/src/main/java/org/distorted/solverui/ScreenList.java b/src/main/java/org/distorted/solverui/ScreenList.java
deleted file mode 100644
index 6033426c..00000000
--- a/src/main/java/org/distorted/solverui/ScreenList.java
+++ /dev/null
@@ -1,151 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import static org.distorted.objectlib.main.ObjectControl.MODE_DRAG;
-import static org.distorted.objectlib.main.ObjectControl.MODE_REPLACE;
-
-import android.content.SharedPreferences;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public enum ScreenList
-  {
-  SVER ( null , MODE_REPLACE, new ScreenSetupPosition()        ),
-  SOLU ( SVER , MODE_DRAG   , new ScreenSolutionSinglephased() ),
-  PHAS ( SVER , MODE_DRAG   , new ScreenSolutionMultiphased()  ),
-  ;
-
-  public static final int LENGTH = values().length;
-  private static final ScreenList[] screens;
-  private final ScreenList mBack;
-  private final int mMode;
-  private final ScreenAbstract mClass;
-
-  private static ScreenList mCurrScreen;
-
-  static
-    {
-    int i = 0;
-    screens = new ScreenList[LENGTH];
-    for(ScreenList state: ScreenList.values()) screens[i++] = state;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getScreen(int ordinal)
-    {
-    return ordinal>=0 && ordinal<LENGTH ?  screens[ordinal] : SVER;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getScreenFromName(String name)
-    {
-    for(int i=0; i<LENGTH; i++)
-      {
-      if( name.equals(screens[i].name()) )
-        {
-        return screens[i];
-        }
-      }
-
-    return SVER;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static ScreenList getCurrentScreen()
-    {
-    return mCurrScreen;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static int getMode()
-    {
-    return mCurrScreen.mMode;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void savePreferences(SharedPreferences.Editor editor)
-    {
-    editor.putString("curr_state_name", mCurrScreen.name() );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void restorePreferences(SharedPreferences preferences)
-    {
-    String currScreenName = preferences.getString("curr_state_name", ScreenList.SVER.name() );
-    mCurrScreen = getScreenFromName(currScreenName);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void goBack(SolverActivity act)
-    {
-    switchScreen(act, mCurrScreen.mBack );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void setScreen(SolverActivity act)
-    {
-    mCurrScreen.enterScreen(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public static void switchScreen(SolverActivity act, ScreenList next)
-    {
-    if( next!=null )
-      {
-      if( mCurrScreen !=null ) mCurrScreen.leaveScreen(act);
-      next.enterScreen(act);
-      mCurrScreen = next;
-      }
-    else
-      {
-      act.finish();
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  ScreenList(ScreenList back, int mode, ScreenAbstract clazz)
-    {
-    mBack = back;
-    mMode = mode;
-    mClass= clazz;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public ScreenAbstract getScreenClass()
-    {
-    return mClass;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void leaveScreen(SolverActivity act)
-    {
-    mClass.leaveScreen(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void enterScreen(SolverActivity act)
-    {
-    mClass.enterScreen(act);
-    }
-  }
\ No newline at end of file
diff --git a/src/main/java/org/distorted/solverui/ScreenSetupPosition.java b/src/main/java/org/distorted/solverui/ScreenSetupPosition.java
deleted file mode 100644
index 3fb995fd..00000000
--- a/src/main/java/org/distorted/solverui/ScreenSetupPosition.java
+++ /dev/null
@@ -1,845 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import static org.distorted.objectlib.metadata.ListObjects.*;
-import static org.distorted.objectlib.solvers.verifiers.SolverTablebase.*;
-
-import android.content.SharedPreferences;
-import android.content.res.Resources;
-import android.graphics.Bitmap;
-import android.graphics.Canvas;
-import android.graphics.Paint;
-import android.graphics.PorterDuff;
-import android.graphics.drawable.Drawable;
-import android.os.Bundle;
-import android.view.View;
-import android.widget.ImageButton;
-import android.widget.LinearLayout;
-
-import androidx.core.content.ContextCompat;
-
-import org.distorted.dialogs.DialogSolverError;
-import org.distorted.dialogs.DialogSolverImpossible;
-import org.distorted.dialogs.DialogSolvers;
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.MainActivity;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.OperatingSystemInterface;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-import org.distorted.objectlib.metadata.ListObjects;
-import org.distorted.objectlib.shape.*;
-import org.distorted.objectlib.solvers.verifiers.ResultScreen;
-import org.distorted.objectlib.solvers.verifiers.SolverAbstract;
-import org.distorted.objectlib.solvers.verifiers.SolvingList;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenSetupPosition extends ScreenAbstract implements ResultScreen
-  {
-  private static final int RESET_DURATION = 1000;
-  private static final int MODE_NORMAL = 0;
-  private static final int MODE_DINO_4 = 1;
-
-  private static final int[][] colorsHex =
-    {
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
-    };
-  private static final int[][] colorsTet =
-    {
-      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-    };
-  private static final int[][] colorsOct =
-    {
-      {R.string.color_violet1,R.string.color_violet2,R.string.color_violet3,R.string.color_violet4,R.string.color_violet5,R.string.color_violet6,R.string.color_violet7 },
-      {R.string.color_grey1  ,R.string.color_grey2  ,R.string.color_grey3  ,R.string.color_grey4  ,R.string.color_grey5  ,R.string.color_grey6  ,R.string.color_grey7   },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-      {R.string.color_orange1,R.string.color_orange2,R.string.color_orange3,R.string.color_orange4,R.string.color_orange5,R.string.color_orange6,R.string.color_orange7 },
-      {R.string.color_green1 ,R.string.color_green2 ,R.string.color_green3 ,R.string.color_green4 ,R.string.color_green5 ,R.string.color_green6 ,R.string.color_green7  },
-      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-    };
-  private static final int[][] colorsDi4 =
-    {
-      {R.string.color_yellow1,R.string.color_yellow2,R.string.color_yellow3,R.string.color_yellow4,R.string.color_yellow5,R.string.color_yellow6,R.string.color_yellow7 },
-      {R.string.color_white1 ,R.string.color_white2 ,R.string.color_white3 ,R.string.color_white4 ,R.string.color_white5 ,R.string.color_white6 ,R.string.color_white7  },
-      {R.string.color_blue1  ,R.string.color_blue2  ,R.string.color_blue3  ,R.string.color_blue4  ,R.string.color_blue5  ,R.string.color_blue6  ,R.string.color_blue7   },
-      {R.string.color_red1   ,R.string.color_red2   ,R.string.color_red3   ,R.string.color_red4   ,R.string.color_red5   ,R.string.color_red6   ,R.string.color_red7    },
-    };
-
-  private static Bitmap[] mBitmap;
-  private ImageButton[] mColorButton;
-  private TransparentImageButton mResetButton,mBackButton, mSolveButton;
-  private boolean mSolving;
-  private int mCurrentColor, mCurrentButton;
-  private int[] mFaceColors;
-  private int mColorMode;
-  private int mNumColors;
-  private int mNumBitmapRows;
-  private float mBitmapSize;
-  private WeakReference<SolverActivity> mWeakAct;
-  private int mObjectOrdinal;
-  private String[] mPhaseNames;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.unsetLock();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.setLock(false);
-
-    float width = act.getScreenWidthInPixels();
-    float heigh = act.getScreenHeightInPixels();
-
-    mWeakAct = new WeakReference<>(act);
-    mSolving = false;
-    mPhaseNames = null;
-
-    mObjectOrdinal = act.getObjectOrdinal();
-    control.solveOnly();
-    generateFaceColors(mObjectOrdinal);
-
-    mNumBitmapRows = mNumColors>8 ? 2 : 1;
-    mBitmapSize = computeBitmapSize(width,heigh);
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    if( mNumColors>0 )
-      {
-      setupBitmaps();
-      setupColorButtons(act);
-      markButton(act);
-      addButtonsToTopLayout(act,layoutTop);
-      }
-
-    // BOT ////////////////////////////
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-
-    LinearLayout layoutL = new LinearLayout(act);
-    layoutL.setLayoutParams(params);
-    LinearLayout layoutM = new LinearLayout(act);
-    layoutM.setLayoutParams(params);
-    LinearLayout layoutR = new LinearLayout(act);
-    layoutR.setLayoutParams(params);
-
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    setupResetButton(act);
-    setupSolveButton(act);
-    setupBackButton(act);
-
-    layoutL.addView(mResetButton);
-    layoutM.addView(mSolveButton);
-    layoutR.addView(mBackButton);
-
-    layoutBot.addView(layoutL);
-    layoutBot.addView(layoutM);
-    layoutBot.addView(layoutR);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private float computeBitmapSize(float width, float heigh)
-    {
-    final float BUTTON_RATIO = 0.75f;
-    float sizeV = (heigh/mNumBitmapRows)*MainActivity.RATIO_BAR;
-    float sizeH = (width*mNumBitmapRows)/mNumColors;
-
-    return BUTTON_RATIO*Math.min(sizeV,sizeH);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void addButtonsToTopLayout(SolverActivity act, LinearLayout layout)
-    {
-    if( mNumBitmapRows==1 )
-      {
-      for(ImageButton button: mColorButton) layout.addView(button);
-      }
-    else if( mNumBitmapRows==2 )
-      {
-      LinearLayout layoutV = new LinearLayout(act);
-      layoutV.setOrientation(LinearLayout.VERTICAL);
-      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1);
-      layoutV.setLayoutParams(params);
-      LinearLayout layoutT = new LinearLayout(act);
-      layoutT.setOrientation(LinearLayout.HORIZONTAL);
-      LinearLayout layoutB = new LinearLayout(act);
-      layoutB.setOrientation(LinearLayout.HORIZONTAL);
-
-      int numB = mColorButton.length;
-      for(int b=0     ; b<numB/2; b++) layoutT.addView(mColorButton[b]);
-      for(int b=numB/2; b<numB  ; b++) layoutB.addView(mColorButton[b]);
-
-      layoutV.addView(layoutT);
-      layoutV.addView(layoutB);
-      layout.addView(layoutV);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// This doesn't quite work in many cases, but in case of the solvers that will pop up in foreseeable
-// future it should be ok.
-
-  public void generateFaceColors(int object)
-    {
-    mColorMode = MODE_NORMAL;
-
-    if( object==PYRA_3.ordinal() ||
-        object==PYRA_4.ordinal() ||
-        object==PYRA_5.ordinal() ||
-        object==PDUO_2.ordinal() ||
-        object==JING_2.ordinal() ||
-        object==MORP_2.ordinal() ||
-        object==MORP_3.ordinal() ||
-        object==MORP_4.ordinal()  )
-      {
-      mNumColors  = ShapeTetrahedron.NUM_FACES;
-      mFaceColors = ShapeTetrahedron.FACE_COLORS;
-      }
-    else if( object==DIAM_2.ordinal() ||
-             object==DIAM_3.ordinal() ||
-             object==DIAM_4.ordinal() ||
-             object==TRAJ_3.ordinal() ||
-             object==TRAJ_4.ordinal() ||
-             object==PDIA_3.ordinal()  )
-      {
-      mNumColors  = ShapeOctahedron.NUM_FACES;
-      mFaceColors = ShapeOctahedron.FACE_COLORS;
-      }
-    else if( object==CRYS_3.ordinal() ||
-             object==STAR_3.ordinal() ||
-             object==PENT_2.ordinal() ||
-             object==KILO_3.ordinal() ||
-             object==KILO_5.ordinal() ||
-             object==MEGA_3.ordinal() ||
-             object==MEGA_5.ordinal()  )
-      {
-      mNumColors  = ShapeDodecahedron.NUM_FACES;
-      mFaceColors = ShapeDodecahedron.FACE_COLORS;
-      }
-    else if( object==BALL_4.ordinal() )
-      {
-      mNumColors  = ShapeDiamond.NUM_FACES;
-      mFaceColors = ShapeDiamond.FACE_COLORS;
-      }
-    else if( object==ICOS_2.ordinal() )
-      {
-      mNumColors  = ShapeIcosahedron.NUM_FACES;
-      mFaceColors = ShapeIcosahedron.FACE_COLORS;
-      }
-    else if( object==DIN4_3.ordinal() )
-      {
-      mNumColors  = 4;
-      mFaceColors = new int[] { ShapeColors.COLOR_YELLOW, ShapeColors.COLOR_RED, ShapeColors.COLOR_BLUE, ShapeColors.COLOR_WHITE};
-      mColorMode  = MODE_DINO_4;
-      }
-    else
-      {
-      mNumColors  = ShapeHexahedron.NUM_FACES;
-      mFaceColors = ShapeHexahedron.FACE_COLORS;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBitmaps()
-    {
-    final int SIZE = (int)mBitmapSize;
-    final float R = SIZE*0.15f;
-    final float M = SIZE*0.08f;
-
-    mBitmap = new Bitmap[mNumColors];
-
-    Paint paint = new Paint();
-    paint.setColor(0xff008800);
-    paint.setStyle(Paint.Style.FILL);
-
-    paint.setAntiAlias(true);
-    paint.setTextAlign(Paint.Align.CENTER);
-    paint.setStyle(Paint.Style.FILL);
-
-    for(int i=0; i<mNumColors; i++)
-      {
-      mBitmap[i] = Bitmap.createBitmap(SIZE, SIZE, Bitmap.Config.ARGB_8888);
-      Canvas canvas = new Canvas(mBitmap[i]);
-
-      paint.setColor(0xff000000);
-      canvas.drawRect(0, 0, SIZE, SIZE, paint);
-
-      paint.setColor(mFaceColors[i]);
-      canvas.drawRoundRect( M, M, SIZE-M, SIZE-M, R, R, paint);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int translateColor(int color)
-    {
-    if( mColorMode==MODE_DINO_4 )
-      {
-      int realColor = mFaceColors[color];
-      int[] hexColors = ShapeHexahedron.FACE_COLORS;
-
-      for(int i=0; i<6; i++)
-        if( hexColors[i]==realColor ) return i;
-      }
-
-    return color;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private boolean pressSolve(SolverActivity act)
-    {
-    int[] solverOrdinals = SolvingList.getSolverOrdinals(mObjectOrdinal);
-
-    if( solverOrdinals!=null  )
-      {
-      SolvingList slvList = SolvingList.getSolver(solverOrdinals[0]);
-      OperatingSystemInterface os = act.getInterface();
-      TwistyObject object = act.getObject();
-      SolverAbstract solver = slvList.create(os,object);
-
-      if( solver!=null )
-        {
-        int[] result = solver.validatePosition(object);
-
-        if( result[0]>=0 ) // position is valid
-          {
-          if( solverOrdinals.length==1 ) // just one solver - simply launch it
-            {
-            solver.solve(this,result);
-            return true;
-            }
-          else // more than one solver - pop up a choosing dialog
-            {
-            ListObjects objList = ListObjects.getObject(mObjectOrdinal);
-            String upperName = objList.name();
-            Bundle bundle = new Bundle();
-            bundle.putString("argument", upperName );
-            DialogSolvers solv = new DialogSolvers();
-            solv.setArguments(bundle);
-            solv.show( act.getSupportFragmentManager(), DialogSolvers.getDialogTag());
-            return false;
-            }
-          }
-        else
-          {
-          displayImpossibleDialog(result,solver.getFaceColors());
-          return false;
-          }
-        }
-      else
-        {
-        displayErrorDialog(act.getString(R.string.solver_generic_not_implemented));
-        return false;
-        }
-      }
-    else  // no solvers? Impossible!
-      {
-      displayErrorDialog("No solvers found for object "+mObjectOrdinal);
-      return false;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void fail(String result) {}
-  public void setPhaseNames(String[] names) { mPhaseNames = names; }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void startedSolving()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void stoppedSolving()
-    {
-
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupColorButtons(final SolverActivity act)
-    {
-    mColorButton = new ImageButton[mNumColors];
-
-    for(int i=0; i<mNumColors; i++)
-      {
-      final int ii = i;
-      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.WRAP_CONTENT,LinearLayout.LayoutParams.MATCH_PARENT, 1.0f);
-
-      mColorButton[i] = new ImageButton(act);
-      mColorButton[i].setLayoutParams(params);
-      mColorButton[i].setImageBitmap(mBitmap[i]);
-
-      mColorButton[i].setOnClickListener( new View.OnClickListener()
-        {
-        @Override
-        public void onClick(View view)
-          {
-          mCurrentColor = translateColor(ii);
-          mCurrentButton= ii;
-          markButton(act);
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupResetButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
-    mResetButton = new TransparentImageButton(act, R.drawable.ui_reset, params);
-
-    mResetButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        control.resetTextureMapsEffect(RESET_DURATION);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupSolveButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mSolveButton = new TransparentImageButton(act,R.drawable.ui_solve,params);
-
-    mSolveButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        if( !mSolving && pressSolve(act) ) mSolving = true;
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        control.resetAllTextureMaps();
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void markButton(SolverActivity act)
-    {
-    if( mCurrentButton>=mNumColors )
-      {
-      mCurrentButton = 0;
-      mCurrentColor = translateColor(0);
-      }
-
-    for(int b=0; b<mNumColors; b++)
-      {
-      Drawable d = mColorButton[b].getBackground();
-      int s = b==mCurrentButton ? act.getSelectedColor() : act.getNormalColor();
-      d.setColorFilter(ContextCompat.getColor(act,s), PorterDuff.Mode.MULTIPLY);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor)
-    {
-    editor.putInt("stateSolver_color" , mCurrentColor );
-    editor.putInt("stateSolver_button", mCurrentButton);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void restorePreferences(SharedPreferences preferences)
-    {
-    mCurrentColor = preferences.getInt("stateSolver_color" , 0);
-    mCurrentButton= preferences.getInt("stateSolver_button", 0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public int getCurrentColor()
-    {
-    return mCurrentColor;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setSolved(final String moves)
-    {
-    mSolving = false;
-    final SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      act.runOnUiThread(new Runnable()
-        {
-        @Override
-        public void run()
-          {
-          ScreenList.switchScreen(act, ScreenList.SOLU);
-          ScreenSolutionSinglephased solution = (ScreenSolutionSinglephased) ScreenList.SOLU.getScreenClass();
-          solution.setSolution(moves);
-          if( !moves.isEmpty() ) act.doNotShowDialogAnymore();
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setSolved(final int[][] moves, final int phaseNumber, final int[][] subphases)
-    {
-    mSolving = false;
-    final SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      act.runOnUiThread(new Runnable()
-        {
-        @Override
-        public void run()
-          {
-          if( mPhaseNames!=null )
-            {
-            ScreenSolutionMultiphased screen = (ScreenSolutionMultiphased) ScreenList.PHAS.getScreenClass();
-            if( phaseNumber==0 )
-              {
-              ScreenList.switchScreen(act, ScreenList.PHAS);
-              screen.updateNames(mPhaseNames);
-              }
-            screen.setSolution(moves, phaseNumber,subphases);
-            }
-          else
-            {
-            ScreenList.switchScreen(act, ScreenList.SOLU);
-            ScreenSolutionSinglephased screen = (ScreenSolutionSinglephased) ScreenList.SOLU.getScreenClass();
-            screen.setSolution(moves);
-            }
-
-          if( moves!=null && moves.length>0 ) act.doNotShowDialogAnymore();
-          }
-        });
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void displayErrorDialog(String message)
-    {
-    mSolving = false;
-    SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      DialogSolverError dialog = new DialogSolverError();
-      Bundle bundle = new Bundle();
-      bundle.putString("argument", message );
-      dialog.setArguments(bundle);
-      dialog.show( act.getSupportFragmentManager(), null);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void displayImpossibleDialog(String message)
-    {
-    mSolving = false;
-    SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      DialogSolverImpossible dialog = new DialogSolverImpossible();
-      Bundle bundle = new Bundle();
-      bundle.putString("argument", message );
-      dialog.setArguments(bundle);
-      dialog.show( act.getSupportFragmentManager(), null);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void displayImpossibleDialog(int[] errorCode, int[] faceColors)
-    {
-    mSolving = false;
-    SolverActivity act = mWeakAct.get();
-
-    if( act!=null )
-      {
-      String message = error(act.getResources(),errorCode,faceColors);
-      displayImpossibleDialog(message);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  int getHexColor(int color,int variant) { return colorsHex[color][variant]; }
-  int getTetColor(int color,int variant) { return colorsTet[color][variant]; }
-  int getOctColor(int color,int variant) { return colorsOct[color][variant]; }
-  int getDi4Color(int color,int variant) { return colorsDi4[color][variant]; }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexCornerMissingError(Resources res, int face0, int face1, int face2)
-    {
-    int j0 = getHexColor(face0,3);
-    int j1 = getHexColor(face1,3);
-    int j2 = getHexColor(face2,4);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexCenterMissingError(Resources res, int face)
-    {
-    int color = getHexColor(face,2);
-    String clr= res.getString(color);
-    return res.getString(R.string.solver_generic_missing_center,clr);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeMissingError(Resources res, int face0, int face1)
-    {
-    int j0 = getHexColor(face0,3);
-    int j1 = getHexColor(face1,6);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeTwistedError(Resources res, int color0, int color1)
-    {
-    int j0 = getHexColor(color0,3);
-    int j1 = getHexColor(color1,6);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_twisted_edge,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexCornerTwistedError(Resources res, int color0, int color1, int color2)
-    {
-    int j0 = getHexColor(color0,3);
-    int j1 = getHexColor(color1,3);
-    int j2 = getHexColor(color2,5);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_twisted_corner,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeMonoError(Resources res, int color)
-    {
-    int j0 = getHexColor(color,3);
-    int j1 = getHexColor(color,6);
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_edge_mono,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String hexEdgeTwiceError(Resources res, int color0, int color1)
-    {
-    int j0 = getHexColor(color0,3);
-    int j1 = getHexColor(color1,6);
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_edge_twice,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String octCenterMissingError(Resources res, int face)
-    {
-    int index = getOctColor(face,2);
-    String color = res.getString(index);
-    return res.getString(R.string.solver_generic_missing_center,color);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String octCornerMissingError(Resources res, int f1, int f2)
-    {
-    int i1 = getOctColor(f1,3);
-    int i2 = getOctColor(f2,4);
-    String c1 = res.getString(i1);
-    String c2 = res.getString(i2);
-    return res.getString(R.string.solver_generic_missing_corner2,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetCornerMissingError(Resources res, int color0, int color1, int color2)
-    {
-    int j0 = getTetColor(color0,3);
-    int j1 = getTetColor(color1,3);
-    int j2 = getTetColor(color2,4);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_missing_corner,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetEdgeMissingError(Resources res, int face0, int face1)
-    {
-    int j0 = getTetColor(face0,3);
-    int j1 = getTetColor(face1,6);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-
-    return res.getString(R.string.solver_generic_missing_edge,c0,c1);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetCenterMissingError(Resources res, int face)
-    {
-    int j = getTetColor(face,2);
-    String c = res.getString(j);
-    return res.getString(R.string.solver_generic_missing_center,c);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String tetVertexMissingError(Resources res, int color0, int color1, int color2)
-    {
-    int j0 = getTetColor(color0,3);
-    int j1 = getTetColor(color1,3);
-    int j2 = getTetColor(color2,4);
-
-    String c0 = res.getString(j0);
-    String c1 = res.getString(j1);
-    String c2 = res.getString(j2);
-
-    return res.getString(R.string.solver_generic_missing_vertex,c0,c1,c2);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  String di4EdgeThreeError(Resources res, int color)
-    {
-    int j0 = getDi4Color(color,7);
-    String c0 = res.getString(j0);
-    return res.getString(R.string.solver_generic_edge_three,c0);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public String error(Resources res, int[] err, int[] faceColors)
-    {
-    switch(err[0])
-      {
-      case ERROR_HEX_CORNER_MISSING    : return hexCornerMissingError(res,err[1],err[2],err[3]);
-      case ERROR_HEX_CENTER_MISSING    : return hexCenterMissingError(res,err[1]);
-      case ERROR_HEX_EDGE_MISSING      : return hexEdgeMissingError(res,err[1],err[2]);
-      case ERROR_HEX_EDGE_TWISTED      : return hexEdgeTwistedError(res,err[1],err[2]);
-      case ERROR_HEX_EDGE_MONOCHROMATIC: return hexEdgeMonoError(res,err[1]);
-      case ERROR_HEX_EDGE_TWICE        : return hexEdgeTwiceError(res,err[1],err[2]);
-      case ERROR_HEX_CORNER_TWISTED    : return hexCornerTwistedError(res,err[1],err[2],err[3]);
-
-      case ERROR_TET_CORNER_MISSING    : return tetCornerMissingError(res,err[1],err[2],err[3]);
-      case ERROR_TET_VERTEX_MISSING    : return tetVertexMissingError(res,err[1],err[2],err[3]);
-      case ERROR_TET_EDGE_MISSING      : return tetEdgeMissingError(res,faceColors[err[1]],faceColors[err[2]]);
-      case ERROR_TET_CENTER_MISSING    : return tetCenterMissingError(res,err[1]);
-
-      case ERROR_OCT_CENTER_MISSING    : return octCenterMissingError(res,err[1]);
-      case ERROR_OCT_CORNER_MISSING    : return octCornerMissingError(res,err[1],err[2]);
-
-      case ERROR_DI4_EDGE_THREE        : return di4EdgeThreeError(res,err[1]);
-
-      case ERROR_CORNERS_CANNOT        : return res.getString(R.string.solver_generic_corners_cannot);
-      case ERROR_EDGE_CANNOT           : return res.getString(R.string.solver_generic_edges_cannot);
-      case ERROR_CORNER_TWISTED        : return res.getString(R.string.solver_generic_corner_twist);
-      case ERROR_CORNER_TWIST_90       : return res.getString(R.string.solver_generic_corner_twist) + " (90)";
-      case ERROR_CORNER_TWIST_180      : return res.getString(R.string.solver_generic_corner_twist) + " (180)";
-      case ERROR_EDGE_TWISTED          : return res.getString(R.string.solver_generic_edge_twist);
-      case ERROR_TWO_CENTERS           : return res.getString(R.string.solver_generic_two_centers);
-      case ERROR_TWO_CORNERS           : return res.getString(R.string.solver_generic_two_corners);
-      case ERROR_TWO_EDGES             : return res.getString(R.string.solver_generic_two_edges);
-      case ERROR_FREE_CORNERS_NOT_EVEN : return res.getString(R.string.solver_generic_free_corners_odd);
-      case ERROR_FREE_CORNERS_ROTATED  : return res.getString(R.string.solver_generic_free_corners_rotated);
-      case ERROR_VERTICES_CANNOT       : return res.getString(R.string.solver_generic_vertices_cannot);
-      case ERROR_C_V_DONT_MATCH        : return res.getString(R.string.solver_generic_c_v_dont_match);
-      case ERROR_TWO_CORNERS_TWO_EDGES : return res.getString(R.string.solver_two_corners_two_edges);
-      }
-
-    return null;
-    }
-  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSolutionMultiphased.java b/src/main/java/org/distorted/solverui/ScreenSolutionMultiphased.java
deleted file mode 100644
index 62a3c48e..00000000
--- a/src/main/java/org/distorted/solverui/ScreenSolutionMultiphased.java
+++ /dev/null
@@ -1,578 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2024 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.library.effect.PostprocessEffectGlow;
-import org.distorted.library.main.DistortedEffects;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.library.message.EffectListener;
-import org.distorted.library.type.Dynamic2D;
-import org.distorted.library.type.Dynamic4D;
-import org.distorted.library.type.Static2D;
-import org.distorted.library.type.Static4D;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.MovesFinished;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenSolutionMultiphased extends ScreenAbstract implements MovesFinished, EffectListener
-  {
-  private static final int MOVES_PLACE_0 = 100;
-  private static final int MOVES_PLACE_1 = 101;
-  private static final int FLASH_TIME = 1200;
-  private static final int MILLIS_PER_DEGREE = 6;
-
-  private WeakReference<SolverActivity> mAct;
-  private TransparentImageButton mPrevButton, mNextButton, mBackButton, mPrevPhase, mNextPhase;
-  private float mButtonSize;
-
-  private TextView mMovesText, mMovesPhase;
-  private String[] mPhaseNames;
-  private int mNumPhases;
-  private int[][][] mMoves;
-  private int[][] mCubitsNotInvolved;
-  private int mNumMoves,mCurrMove,mCurrPhase;
-  private boolean mCanMove;
-
-  private Dynamic2D mHaloAndRadiusDyn;
-  private Dynamic4D mColorDyn;
-  private PostprocessEffectGlow mGlow;
-  private boolean mEffectWorking;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.solveOnly();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final SolverActivity act)
-    {
-    mAct = new WeakReference<>(act);
-
-    mHaloAndRadiusDyn = new Dynamic2D(FLASH_TIME,1.0f);
-    mHaloAndRadiusDyn.add(new Static2D( 0,0));
-    mHaloAndRadiusDyn.add(new Static2D(10,5));
-
-    mColorDyn = new Dynamic4D(FLASH_TIME,1.0f);
-
-    final int[] colors  = new int[] {1,1,1}; // white
-
-    Static4D P1 = new Static4D(colors[0],colors[1],colors[2], 0.0f);
-    Static4D P2 = new Static4D(colors[0],colors[1],colors[2], 1.0f);
-    mColorDyn.add(P1);
-    mColorDyn.add(P2);
-
-    mGlow = new PostprocessEffectGlow(mHaloAndRadiusDyn,mColorDyn);
-
-    float width = act.getScreenWidthInPixels();
-    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    setupPrevPhase(act);
-    setupNextPhase(act);
-    setupTextPhase(act,width);
-
-    layoutTop.addView(mPrevPhase);
-    layoutTop.addView(mMovesPhase);
-    layoutTop.addView(mNextPhase);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutMid = new LinearLayout(act);
-    layoutMid.setLayoutParams(paramsM);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupPrevButton(act);
-    setupNextButton(act);
-    setupTextView(act,width);
-
-    layoutLeft.addView(mPrevButton);
-    layoutLeft.addView(mMovesText);
-    layoutLeft.addView(mNextButton);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutMid);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { backMove(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { nextMove(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextView(final SolverActivity act, final float width)
-    {
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesText = new TextView(act);
-    mMovesText.setTextSize(20);
-    mMovesText.setLayoutParams(params);
-    mMovesText.setPadding(padding,0,padding,0);
-    mMovesText.setGravity(Gravity.CENTER);
-    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevPhase(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevPhase = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevPhase.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { backPhase(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextPhase(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextPhase = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextPhase.setOnClickListener( new View.OnClickListener()
-      {
-      @Override public void onClick(View v) { nextPhase(); }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextPhase(final SolverActivity act, final float width)
-    {
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesPhase = new TextView(act);
-    mMovesPhase.setTextSize(20);
-    mMovesPhase.setLayoutParams(params);
-    mMovesPhase.setPadding(padding,0,padding,0);
-    mMovesPhase.setGravity(Gravity.CENTER);
-    mMovesPhase.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesPhase.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor) { }
-  public void restorePreferences(SharedPreferences preferences) { }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void backPhase()
-    {
-    SolverActivity act = mAct.get();
-    ObjectControl control = act.getControl();
-
-    if( mCurrMove>0 )
-      {
-      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mCurrMove, false);
-      control.applyScrambles(moves);
-      mCurrMove = 0;
-      }
-    else if( mCurrPhase>0 )
-      {
-      mCurrPhase--;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = 0;
-      int[][] moves = transformMoves(mMoves[mCurrPhase],0,mNumMoves, false);
-      control.applyScrambles(moves);
-      }
-    else
-      {
-      mCurrPhase = mNumPhases-1;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = mNumMoves;
-      int[][] moves = transformMoves(mMoves, true);
-      control.applyScrambles(moves);
-      }
-
-    setText(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void nextPhase()
-    {
-    SolverActivity act = mAct.get();
-    ObjectControl control = act.getControl();
-
-    if( mCurrPhase<mNumPhases-1 )
-      {
-      glowCubits(mCubitsNotInvolved[mCurrPhase]);
-      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
-      control.applyScrambles(moves);
-      mCurrPhase++;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = 0;
-      }
-    else if( mCurrMove<mNumMoves )
-      {
-      glowCubits(mCubitsNotInvolved[mCurrPhase]);
-      int[][] moves = transformMoves(mMoves[mCurrPhase],mCurrMove,mNumMoves, true);
-      control.applyScrambles(moves);
-      mCurrMove = mNumMoves;
-      }
-    else
-      {
-      mCurrPhase = 0;
-      mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-      mCurrMove = 0;
-      int[][] moves = transformMoves(mMoves, false);
-      control.applyScrambles(moves);
-      }
-
-    setText(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setText(SolverActivity act)
-    {
-    int currMove = 0;
-    int totalMove = 0;
-
-    if( mMoves!=null )
-      {
-      currMove = mCurrMove;
-      for(int p=0; p<mCurrPhase; p++) currMove  += (mMoves[p]==null ? 0: mMoves[p].length);
-      for(int p=0; p<mNumPhases; p++) totalMove += (mMoves[p]==null ? 0: mMoves[p].length);
-      }
-
-    final int cMove = currMove;
-    final int tMove = totalMove;
-
-    act.runOnUiThread(new Runnable()
-      {
-      @Override
-      public void run()
-        {
-        mMovesPhase.setText(mPhaseNames[mCurrPhase]+" "+mCurrMove+"/"+mNumMoves);
-        mMovesText.setText(cMove+"/"+tMove);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int[][] transformMoves(int[][] moves, int start, int end, boolean front)
-    {
-    int mult = front ? 1:-1;
-    int len = end-start;
-    int[][] ret = new int[len][];
-
-    for(int m=0; m<len; m++)
-      {
-      int[] mv = moves[front ? start+m : end-1-m];
-      int[] rt = new int[3];
-      rt[0] = mv[0];
-      rt[1] = (1<<mv[1]);
-      rt[2] = mult*mv[2];
-      ret[m] = rt;
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int[][] transformMoves(int[][][] moves, boolean front)
-    {
-    int len = moves.length;
-    int totalLen = 0;
-    for (int[][] move : moves) totalLen += (move==null ? 0 : move.length);
-
-    int[][] ret = new int[totalLen][];
-    int mult = front ? 1:-1;
-    int index = 0;
-
-    for(int m=0; m<len; m++)
-      {
-      int[][] mv = moves[front ? m : len-1-m];
-      int l = (mv==null ? 0 : mv.length);
-
-      for(int p=0; p<l; p++)
-        {
-        int[] mve = mv[front ? p : l-1-p];
-        int[] rt = new int[3];
-        rt[0] = mve[0];
-        rt[1] = (1<<mve[1]);
-        rt[2] = mult*mve[2];
-        ret[index++] = rt;
-        }
-      }
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void glowCubits(int[] cubits)
-    {
-    if( !mEffectWorking )
-      {
-      mEffectWorking = true;
-      SolverActivity act=mAct.get();
-      ObjectControl control = act.getControl();
-      TwistyObject object=control.getObject();
-      DistortedEffects effects=object.getObjectEffects();
-      effects.apply(mGlow);
-
-      MeshBase mesh=object.getObjectMesh();
-      mesh.setComponentsNotAffectedByPostprocessing(cubits);
-
-      mHaloAndRadiusDyn.resetToBeginning();
-      mColorDyn.resetToBeginning();
-      mGlow.notifyWhenFinished(this);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int[] computeCubitsNotInvolved(int[][] subphases, int numCubits)
-    {
-    int numCubitsInvolved = 0;
-    boolean[] involved = new boolean[numCubits];
-
-    for(int[] sub : subphases)
-      if( sub!=null )
-        for(int s : sub)
-          {
-          numCubitsInvolved++;
-          involved[s] = true;
-          }
-
-    int[] ret = new int[numCubits-numCubitsInvolved];
-    int index = 0;
-
-    for(int c=0; c<numCubits; c++)
-      if( !involved[c] ) ret[index++] = c;
-
-    return ret;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void backMove()
-    {
-    if( mMoves!=null && mCanMove )
-      {
-      SolverActivity act=mAct.get();
-
-      if( mCurrMove>0 )
-        {
-        mCanMove = false;
-        int[] move = mMoves[mCurrPhase][--mCurrMove];
-        ObjectControl control = act.getControl();
-        control.blockTouch(MOVES_PLACE_0);
-        control.addRotation(this, move[0], (1<<move[1]), -move[2], MILLIS_PER_DEGREE);
-        }
-      else if( mCurrPhase>0 )
-        {
-        mCurrPhase--;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = mNumMoves;
-        glowCubits(mCubitsNotInvolved[mCurrPhase]);
-        }
-      else
-        {
-        mCurrPhase = mNumPhases-1;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = mNumMoves;
-        int[][] moves = transformMoves(mMoves, true);
-        ObjectControl control = act.getControl();
-        control.applyScrambles(moves);
-        glowCubits(mCubitsNotInvolved[mCurrPhase]);
-        }
-
-      setText(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void nextMove()
-    {
-    if( mMoves!=null && mCanMove )
-      {
-      SolverActivity act=mAct.get();
-
-      if( mCurrMove<mNumMoves )
-        {
-        mCanMove = false;
-        int[] move = mMoves[mCurrPhase][mCurrMove++];
-        ObjectControl control = act.getControl();
-        control.blockTouch(MOVES_PLACE_1);
-        control.addRotation(this, move[0], (1<<move[1]), move[2], MILLIS_PER_DEGREE);
-        if( mCurrMove==mNumMoves && mCurrPhase==mNumPhases-1 ) glowCubits(mCubitsNotInvolved[mCurrPhase]);
-        }
-      else if( mCurrPhase<mNumPhases-1 )
-        {
-        mCurrPhase++;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = 0;
-        glowCubits(mCubitsNotInvolved[mCurrPhase-1]);
-        }
-      else
-        {
-        mCurrPhase = 0;
-        mNumMoves = mMoves[mCurrPhase]==null ? 0 : mMoves[mCurrPhase].length;
-        mCurrMove = 0;
-        int[][] moves = transformMoves(mMoves, false);
-        ObjectControl control = act.getControl();
-        control.applyScrambles(moves);
-        }
-
-      setText(act);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void updateNames(String[] names)
-    {
-    mPhaseNames = names;
-    mNumPhases = names.length;
-    mMoves = new int[mNumPhases][][];
-    mCubitsNotInvolved = new int[mNumPhases][];
-    mCanMove = true;
-    if( mAct!=null ) setSolution(null,0,null);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void setSolution(int[][] moves, int phase, int[][] subphases)
-    {
-    SolverActivity act=mAct.get();
-
-    if( subphases!=null )
-      {
-      ObjectControl control=act.getControl();
-      TwistyObject object=control.getObject();
-      int numCubits=object.getNumCubits();
-      mCubitsNotInvolved[phase]= computeCubitsNotInvolved(subphases, numCubits);
-      }
-
-    mMoves[phase] = moves;
-    if( phase==0 ) mNumMoves = (moves==null ? 0 : moves.length);
-    mCurrPhase = 0;
-    mCurrMove = 0;
-
-    setText(act);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onActionFinished(final long effectID)
-    {
-    mCanMove = true;
-    SolverActivity act=mAct.get();
-    ObjectControl control = act.getControl();
-    control.unblockRotation();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void effectFinished(long id)
-    {
-    SolverActivity act=mAct.get();
-    ObjectControl control = act.getControl();
-    TwistyObject object=control.getObject();
-    DistortedEffects effects=object.getObjectEffects();
-    effects.abortById(id);
-    mEffectWorking = false;
-    }
-  }
diff --git a/src/main/java/org/distorted/solverui/ScreenSolutionSinglephased.java b/src/main/java/org/distorted/solverui/ScreenSolutionSinglephased.java
deleted file mode 100644
index c15dd5eb..00000000
--- a/src/main/java/org/distorted/solverui/ScreenSolutionSinglephased.java
+++ /dev/null
@@ -1,284 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.SharedPreferences;
-import android.util.TypedValue;
-import android.view.Gravity;
-import android.view.LayoutInflater;
-import android.view.View;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import org.distorted.helpers.TransparentImageButton;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.MovesFinished;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.patterns.RubikPattern;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class ScreenSolutionSinglephased extends ScreenAbstract implements MovesFinished
-  {
-  private static final int MILLIS_PER_DEGREE = 6;
-
-  private TransparentImageButton mPrevButton, mNextButton, mBackButton;
-  private TextView mMovesText;
-  private int[][] mMoves;
-  private int mCurrMove, mNumMoves;
-  private boolean mCanRotate;
-  private float mButtonSize;
-  private WeakReference<SolverActivity> mAct;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void leaveScreen(SolverActivity act)
-    {
-    ObjectControl control = act.getControl();
-    control.solveOnly();
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void enterScreen(final SolverActivity act)
-    {
-    mAct = new WeakReference<>(act);
-
-    float width = act.getScreenWidthInPixels();
-    mButtonSize = width*SolverActivity.BUTTON_TEXT_SIZE;
-    float titleSize  = width*SolverActivity.TITLE_TEXT_SIZE;
-
-    LayoutInflater inflater = act.getLayoutInflater();
-
-    // TOP ////////////////////////////
-    LinearLayout layoutTop = act.findViewById(R.id.upperBar);
-    layoutTop.removeAllViews();
-
-    final TextView text = (TextView)inflater.inflate(R.layout.upper_text, null);
-    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, titleSize);
-    text.setText(R.string.solution);
-    layoutTop.addView(text);
-
-    // BOT ////////////////////////////
-    LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
-    layoutBot.removeAllViews();
-
-    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams((int)(width/2),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams((int)(width/6),LinearLayout.LayoutParams.MATCH_PARENT);
-    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams((int)(width/3),LinearLayout.LayoutParams.MATCH_PARENT);
-
-    LinearLayout layoutLeft = new LinearLayout(act);
-    layoutLeft.setLayoutParams(paramsL);
-    LinearLayout layoutMid = new LinearLayout(act);
-    layoutMid.setLayoutParams(paramsM);
-    LinearLayout layoutRight = new LinearLayout(act);
-    layoutRight.setLayoutParams(paramsR);
-
-    setupPrevButton(act);
-    setupNextButton(act);
-    setupTextView(act,width);
-
-    layoutLeft.addView(mPrevButton);
-    layoutLeft.addView(mMovesText);
-    layoutLeft.addView(mNextButton);
-
-    setupBackButton(act);
-
-    layoutRight.addView(mBackButton);
-
-    layoutBot.addView(layoutLeft);
-    layoutBot.addView(layoutMid);
-    layoutBot.addView(layoutRight);
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mPrevButton = new TransparentImageButton(act,R.drawable.ui_left,params);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        backMove(control);
-        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupNextButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mNextButton = new TransparentImageButton(act,R.drawable.ui_right,params);
-
-    mNextButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ObjectControl control = act.getControl();
-        makeMove(control);
-        mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupTextView(final SolverActivity act, final float width)
-    {
-    int padding = (int)(width*SolverActivity.PADDING);
-    int margin  = (int)(width*SolverActivity.SMALL_MARGIN);
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
-    params.topMargin    = margin;
-    params.bottomMargin = margin;
-    params.leftMargin   = margin;
-    params.rightMargin  = margin;
-
-    mMovesText = new TextView(act);
-    mMovesText.setTextSize(20);
-    mMovesText.setLayoutParams(params);
-    mMovesText.setPadding(padding,0,padding,0);
-    mMovesText.setGravity(Gravity.CENTER);
-    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupBackButton(final SolverActivity act)
-    {
-    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-    mBackButton = new TransparentImageButton(act,R.drawable.ui_back,params);
-
-    mBackButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        ScreenList.goBack(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void makeMove(ObjectControl control)
-    {
-    if( mCanRotate )
-      {
-      mCurrMove++;
-
-      if( mCurrMove>mNumMoves )
-        {
-        mCurrMove= 0;
-        control.initializeObject(null);
-        }
-      else
-        {
-        int axis      = mMoves[mCurrMove-1][0];
-		int rowBitmap = mMoves[mCurrMove-1][1];
-		int bareAngle = mMoves[mCurrMove-1][2];
-
-        if( bareAngle!=0 )
-          {
-          mCanRotate = false;
-          control.addRotation(this, axis, rowBitmap, bareAngle, MILLIS_PER_DEGREE);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: solution contains angle 0");
-          }
-        }
-      }
-    else
-      {
-      android.util.Log.e("solution", "failed to make move!");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void backMove(ObjectControl control)
-    {
-    if( mCanRotate )
-      {
-      mCurrMove--;
-
-      if( mCurrMove<0 )
-        {
-        mCurrMove=mNumMoves;
-        control.initializeObject(mMoves);
-        }
-      else
-        {
-        int axis      = mMoves[mCurrMove][0];
-		int rowBitmap = mMoves[mCurrMove][1];
-		int bareAngle = mMoves[mCurrMove][2];
-
-        if( bareAngle!=0 )
-          {
-          mCanRotate = false;
-          control.addRotation(this, axis, rowBitmap, -bareAngle, MILLIS_PER_DEGREE);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: solution contains angle 0");
-          }
-        }
-      }
-    else
-      {
-      android.util.Log.e("solution", "failed to back move!");
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setSolution(String moves)
-    {
-    mCanRotate= true;
-    mCurrMove = 0;
-    mNumMoves = moves.length()/4;
-    mMoves    = new int[mNumMoves][3];
-
-    RubikPattern.parseMoves(mMoves,mNumMoves,moves);
-
-    SolverActivity act = mAct.get();
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setSolution(int[][] moves)
-    {
-    mCanRotate= true;
-    mCurrMove = 0;
-    mNumMoves = moves==null ? 0 : moves.length;
-    mMoves    = moves;
-
-    SolverActivity act = mAct.get();
-    mMovesText.setText(act.getString(R.string.mo_placeholder,mCurrMove,mNumMoves));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void savePreferences(SharedPreferences.Editor editor) { }
-  public void restorePreferences(SharedPreferences preferences) { }
-  public void onActionFinished(final long effectID) { mCanRotate = true; }
-  }
diff --git a/src/main/java/org/distorted/solverui/SolverActivity.java b/src/main/java/org/distorted/solverui/SolverActivity.java
deleted file mode 100644
index 876c6f54..00000000
--- a/src/main/java/org/distorted/solverui/SolverActivity.java
+++ /dev/null
@@ -1,239 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.SharedPreferences;
-import android.os.Build;
-import android.os.Bundle;
-import android.view.DisplayCutout;
-import android.view.ViewGroup;
-import android.widget.LinearLayout;
-
-import org.distorted.dialogs.DialogError;
-import org.distorted.dialogs.DialogMessage;
-import org.distorted.helpers.BaseActivity;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.library.main.DistortedScreen;
-import org.distorted.main.R;
-import org.distorted.objectlib.helpers.OperatingSystemInterface;
-import org.distorted.objectlib.main.InitAssets;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObject;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
-import org.distorted.os.OSInterface;
-
-import java.io.InputStream;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class SolverActivity extends BaseActivity
-{
-    public static final float RATIO_UPP       = 0.100f;
-    public static final float PADDING         = 0.010f;
-    public static final float SMALL_MARGIN    = 0.004f;
-    public static final float BUTTON_TEXT_SIZE= 0.050f;
-    public static final float TITLE_TEXT_SIZE = 0.060f;
-
-    private static final int ACTIVITY_NUMBER = 4;
-    private static final float RATIO_INSET= 0.09f;
-
-    private int mObjectOrdinal;
-    private boolean mDisplayMessageDialog;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    protected void onCreate(Bundle savedState)
-      {
-      super.onCreate(savedState);
-      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
-      setContentView(R.layout.solver);
-
-      Bundle b = getIntent().getExtras();
-      mObjectOrdinal = b!=null ? b.getInt("obj") : 0;
-      mDisplayMessageDialog = true;
-
-      computeScreenDimensions();
-      hideNavigationBar();
-      cutoutHack();
-      computeUpperBarHeight(RATIO_UPP);
-      computeLowerBarHeight(RATIO_BAR);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onAttachedToWindow()
-      {
-      super.onAttachedToWindow();
-
-      if( Build.VERSION.SDK_INT >= Build.VERSION_CODES.P )
-        {
-        DisplayCutout cutout = getWindow().getDecorView().getRootWindowInsets().getDisplayCutout();
-        int insetHeight = cutout!=null ? cutout.getSafeInsetTop() : 0;
-
-        LinearLayout layoutHid = findViewById(R.id.hiddenBar);
-        ViewGroup.LayoutParams paramsHid = layoutHid.getLayoutParams();
-        paramsHid.height = (int)(insetHeight*RATIO_INSET);
-        layoutHid.setLayoutParams(paramsHid);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onPause() 
-      {
-      super.onPause();
-      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
-      view.onPause();
-      DistortedLibrary.onPause(ACTIVITY_NUMBER);
-      savePreferences();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onResume() 
-      {
-      super.onResume();
-      DistortedLibrary.onResume(ACTIVITY_NUMBER);
-      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
-      view.onResume();
-
-      createObject();
-
-      restorePreferences();
-      ScreenList.setScreen(this);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-    
-    @Override
-    protected void onDestroy() 
-      {
-      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
-      super.onDestroy();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void savePreferences()
-      {
-      SharedPreferences.Editor editor = mPreferences.edit();
-
-      for( int i=0; i< ScreenList.LENGTH; i++ )
-        {
-        ScreenList.getScreen(i).getScreenClass().savePreferences(editor);
-        }
-
-      ScreenList.savePreferences(editor);
-
-      editor.putBoolean("solverDisplayDialog", mDisplayMessageDialog );
-
-      boolean success = editor.commit();
-      if( !success ) android.util.Log.e("D", "Failed to save preferences");
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    private void restorePreferences()
-      {
-      for( int i=0; i<ScreenList.LENGTH; i++ )
-        {
-        ScreenList.getScreen(i).getScreenClass().restorePreferences(mPreferences);
-        }
-
-      ScreenList.restorePreferences(mPreferences);
-
-      mDisplayMessageDialog = mPreferences.getBoolean("solverDisplayDialog",true);
-
-      if( mDisplayMessageDialog )
-        {
-        Bundle bundle = new Bundle();
-        bundle.putString("argument", getString(R.string.solver_message) );
-        DialogMessage diag = new DialogMessage();
-        diag.setArguments(bundle);
-        diag.show( getSupportFragmentManager(), DialogMessage.getDialogTag());
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void doNotShowDialogAnymore()
-      {
-      mDisplayMessageDialog = false;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void OpenGLError()
-      {
-      DialogError errDiag = new DialogError();
-      errDiag.show(getSupportFragmentManager(), null);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public TwistyObject getObject()
-      {
-      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
-      return view.getObjectControl().getObject();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public DistortedScreen getScreen()
-      {
-      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
-      return view.getRenderer().getScreen();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public ObjectControl getControl()
-      {
-      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
-      return view.getObjectControl();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public int getObjectOrdinal()
-      {
-      return mObjectOrdinal;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void createObject()
-      {
-      SolverSurfaceView view = findViewById(R.id.solverSurfaceView);
-      ObjectControl control = view.getObjectControl();
-      RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
-      InputStream jsonStream = object==null ? null : object.getObjectStream(this);
-      InputStream meshStream = object==null ? null : object.getMeshStream(this);
-      String name = object==null ? "NULL" : object.getUpperName();
-      OSInterface os = view.getInterface();
-      InitAssets asset = new InitAssets(jsonStream,meshStream,os);
-      control.changeIfDifferent(mObjectOrdinal,name,TwistyObject.MODE_NORM,asset);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public OperatingSystemInterface getInterface()
-      {
-      SolverSurfaceView view  = findViewById(R.id.solverSurfaceView);
-      return view.getInterface();
-      }
-}
diff --git a/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java b/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
deleted file mode 100644
index 2da64cbe..00000000
--- a/src/main/java/org/distorted/solverui/SolverObjectLibInterface.java
+++ /dev/null
@@ -1,194 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.library.message.EffectMessageSender;
-import org.distorted.main.BuildConfig;
-import org.distorted.objectlib.helpers.BlockController;
-import org.distorted.objectlib.helpers.ObjectLibInterface;
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objects.RubikObject;
-import org.distorted.objects.RubikObjectList;
-import org.distorted.objectlib.solvers.verifiers.SolvingList;
-
-import java.lang.ref.WeakReference;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class SolverObjectLibInterface implements ObjectLibInterface
-{
-  private final WeakReference<SolverActivity> mAct;
-  private int mLastCubitColor, mLastCubit, mLastCubitFace;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  SolverObjectLibInterface(SolverActivity act)
-    {
-    mAct = new WeakReference<>(act);
-    mLastCubitColor = -1;
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onScrambleEffectFinished() { }
-  public void onRemoveRotation(int axis, int row, int angle) { }
-  public void onBeginRotation() { }
-  public void failedToDrag() { }
-  public void onSolved() { }
-  public void onObjectCreated(long time) { }
-  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportProblem(String problem, boolean recordException)
-    {
-    if( BuildConfig.DEBUG )
-      {
-      android.util.Log.e("libInterface", problem);
-      }
-    else
-      {
-      if( recordException )
-        {
-        Exception ex = new Exception(problem);
-        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-        crashlytics.setCustomKey("problem" , problem);
-        crashlytics.recordException(ex);
-        }
-      else
-        {
-        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-        crashlytics.log(problem);
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportScramblingProblem(int place, long pause, long resume, long time)
-    {
-    String error = "SCRAMBLING BLOCK "+place+" blocked for "+time;
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause );
-      crashlytics.setCustomKey("resume", resume );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportRotationProblem(int place, long pause, long resume, long time)
-    {
-    String error = "ROTATION BLOCK "+place+" blocked for "+time;
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause );
-      crashlytics.setCustomKey("resume", resume);
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void reportThreadProblem(int place, long pause, long resume, long time)
-    {
-    String error = EffectMessageSender.reportState();
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("pause" , pause  );
-      crashlytics.setCustomKey("resume", resume );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
-    {
-    switch(type)
-      {
-      case BlockController.TYPE_SCRAMBLING: reportScramblingProblem(place,pause,resume,time); break;
-      case BlockController.TYPE_ROTATION  : reportRotationProblem(place,pause,resume,time); break;
-      case BlockController.TYPE_THREAD    : reportThreadProblem(place,pause,resume,time); break;
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void reportJSONError(String error, int ordinal)
-    {
-    RubikObject object = RubikObjectList.getObject(ordinal);
-    String name = object==null ? "NULL" : object.getUpperName();
-
-    if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("libInterface", "name="+name+" JSON error: "+error);
-       }
-    else
-      {
-      Exception ex = new Exception(error);
-      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-      crashlytics.setCustomKey("name" , name );
-      crashlytics.setCustomKey("JSONerror", error );
-      crashlytics.recordException(ex);
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onReplaceModeDown(int cubit, int face)
-    {
-    SolverActivity act = mAct.get();
-    ScreenSetupPosition solver = (ScreenSetupPosition) ScreenList.SVER.getScreenClass();
-    int color = solver.getCurrentColor();
-    int currObject = act.getObjectOrdinal();
-    mLastCubitColor = SolvingList.cubitIsLocked(currObject,cubit);
-    mLastCubit = cubit;
-    mLastCubitFace = face;
-    ObjectControl control = act.getControl();
-    control.setTextureMap( cubit, face, color );
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onReplaceModeUp()
-    {
-    if( mLastCubitColor>=0 )
-      {
-      ObjectControl control = mAct.get().getControl();
-      control.setTextureMap( mLastCubit, mLastCubitFace, mLastCubitColor );
-      mLastCubitColor = -1;
-      }
-    }
-}
diff --git a/src/main/java/org/distorted/solverui/SolverRenderer.java b/src/main/java/org/distorted/solverui/SolverRenderer.java
deleted file mode 100644
index 82606acd..00000000
--- a/src/main/java/org/distorted/solverui/SolverRenderer.java
+++ /dev/null
@@ -1,159 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.content.res.Resources;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.library.effect.EffectType;
-import org.distorted.library.effect.VertexEffectQuaternion;
-import org.distorted.library.effect.VertexEffectRotate;
-import org.distorted.library.main.DistortedLibrary;
-import org.distorted.library.main.DistortedScreen;
-import org.distorted.library.main.InternalOutputSurface;
-import org.distorted.library.mesh.MeshBase;
-import org.distorted.main.BuildConfig;
-import org.distorted.objectlib.effects.BaseEffect;
-import org.distorted.objectlib.main.ObjectControl;
-
-import java.io.InputStream;
-
-import javax.microedition.khronos.egl.EGLConfig;
-import javax.microedition.khronos.opengles.GL10;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class SolverRenderer implements GLSurfaceView.Renderer, DistortedLibrary.LibraryUser
-{
-   private final SolverSurfaceView mView;
-   private final Resources mResources;
-   private final DistortedScreen mScreen;
-   private final ObjectControl mControl;
-   private boolean mErrorShown;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   SolverRenderer(SolverSurfaceView v)
-     {
-     mView = v;
-     mResources = v.getResources();
-
-     mErrorShown = false;
-     mControl = v.getObjectControl();
-     mScreen = new DistortedScreen();
-
-     SolverActivity act = (SolverActivity)v.getContext();
-     act.setUpBackgroundColor(mScreen);
-
-     mScreen.enableDepthStencil(InternalOutputSurface.DEPTH_NO_STENCIL);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   DistortedScreen getScreen()
-     {
-     return mScreen;
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// various things are done here delayed, 'after the next render' as not to be done mid-render and
-// cause artifacts.
-
-   @Override
-   public void onDrawFrame(GL10 glUnused)
-     {
-     long time = System.currentTimeMillis();
-     mControl.preRender();
-     mScreen.render(time);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceChanged(GL10 glUnused, int width, int height)
-      {
-      mScreen.resize(width,height);
-      mView.setScreenSize(width,height);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   @Override
-   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
-      {
-      DistortedLibrary.setMax(EffectType.VERTEX,ObjectControl.MAX_QUATS+1);
-      MeshBase.setMaxEffComponents(ObjectControl.MAX_MOVING_PARTS);
-
-      VertexEffectRotate.enable();
-      VertexEffectQuaternion.enable();
-      BaseEffect.Type.enableEffects();
-
-      DistortedLibrary.onSurfaceCreated(this,1);
-      DistortedLibrary.setCull(true);
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void distortedException(Exception ex)
-     {
-     String message = ex.getMessage();
-     String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
-     String version = GLES30.glGetString(GLES30.GL_VERSION);
-     String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
-     String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
-
-     if( message==null ) message = "exception NULL";
-
-     if( BuildConfig.DEBUG )
-       {
-       android.util.Log.e("DISTORTED", message );
-       android.util.Log.e("DISTORTED", "GLSL Version "+shading);
-       android.util.Log.e("DISTORTED", "GL Version "  +version);
-       android.util.Log.e("DISTORTED", "GL Vendor "   +vendor);
-       android.util.Log.e("DISTORTED", "GL Renderer " +renderer);
-       }
-     else
-       {
-       FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-       crashlytics.setCustomKey("DistortedError", message );
-       crashlytics.setCustomKey("GLSL Version"  , shading );
-       crashlytics.setCustomKey("GLversion"     , version );
-       crashlytics.setCustomKey("GL Vendor "    , vendor  );
-       crashlytics.setCustomKey("GLSLrenderer"  , renderer);
-       crashlytics.recordException(ex);
-       }
-
-     int glsl = DistortedLibrary.getGLSL();
-
-     if( glsl< 300 && !mErrorShown )
-       {
-       mErrorShown = true;
-       SolverActivity act = (SolverActivity)mView.getContext();
-       act.OpenGLError();
-       }
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public InputStream localFile(int fileID)
-     {
-     return mResources.openRawResource(fileID);
-     }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-   public void logMessage(String message)
-     {
-     android.util.Log.e("Rubik", message );
-     }
-}
diff --git a/src/main/java/org/distorted/solverui/SolverSurfaceView.java b/src/main/java/org/distorted/solverui/SolverSurfaceView.java
deleted file mode 100644
index 455e5a45..00000000
--- a/src/main/java/org/distorted/solverui/SolverSurfaceView.java
+++ /dev/null
@@ -1,149 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2023 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is proprietary software licensed under an EULA which you should have received      //
-// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.solverui;
-
-import android.annotation.SuppressLint;
-import android.app.ActivityManager;
-import android.content.Context;
-import android.content.pm.ConfigurationInfo;
-import android.opengl.GLES30;
-import android.opengl.GLSurfaceView;
-import android.util.AttributeSet;
-import android.view.MotionEvent;
-
-import com.google.firebase.crashlytics.FirebaseCrashlytics;
-
-import org.distorted.objectlib.main.ObjectControl;
-import org.distorted.objectlib.main.TwistyObjectNode;
-import org.distorted.os.OSInterface;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-public class SolverSurfaceView extends GLSurfaceView
-{
-    private ObjectControl mObjectController;
-    private OSInterface mInterface;
-    private SolverRenderer mRenderer;
-    private boolean mCreated;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    void setScreenSize(int width, int height)
-      {
-      mObjectController.setScreenSizeAndScaling(width,height, Math.min(width, (int)(0.75f*height)));
-
-      if( !mCreated )
-        {
-        mCreated = true;
-        mObjectController.createNode(width,height);
-        TwistyObjectNode objectNode = mObjectController.getNode();
-        objectNode.glDepthMask(false);
-        objectNode.glStencilMask(0);
-        mRenderer.getScreen().attach(objectNode);
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    SolverRenderer getRenderer()
-      {
-      return mRenderer;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    OSInterface getInterface()
-      {
-      return mInterface;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    ObjectControl getObjectControl()
-      {
-      return mObjectController;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public SolverSurfaceView(Context context, AttributeSet attrs)
-      {
-      super(context,attrs);
-
-      mCreated = false;
-
-      if(!isInEditMode())
-        {
-        SolverActivity act = (SolverActivity)context;
-        SolverObjectLibInterface ref = new SolverObjectLibInterface(act);
-        mInterface = new OSInterface(act,ref);
-        mObjectController = new ObjectControl(mInterface);
-        mRenderer = new SolverRenderer(this);
-
-        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
-
-        try
-          {
-          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
-          int esVersion = configurationInfo.reqGlEsVersion>>16;
-          setEGLContextClientVersion(esVersion);
-          setRenderer(mRenderer);
-          }
-        catch(Exception ex)
-          {
-          act.OpenGLError();
-
-          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
-          String version = GLES30.glGetString(GLES30.GL_VERSION);
-          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
-          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
-
-          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
-          crashlytics.setCustomKey("GLSL Version"  , shading );
-          crashlytics.setCustomKey("GL version"    , version );
-          crashlytics.setCustomKey("GL Vendor "    , vendor  );
-          crashlytics.setCustomKey("GLSL renderer" , renderer);
-          crashlytics.recordException(ex);
-          }
-        }
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onPause()
-      {
-      super.onPause();
-      mObjectController.onPause();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @Override
-    public void onResume()
-      {
-      super.onResume();
-      mObjectController.onResume();
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    @SuppressLint("ClickableViewAccessibility")
-    @Override
-    public boolean onTouchEvent(MotionEvent event)
-      {
-      mInterface.setMotionEvent(event);
-      int mode = ScreenList.getMode();
-      return mObjectController.onTouchEvent(mode);
-      }
-}
-
diff --git a/src/main/res/layout/pattern.xml b/src/main/res/layout/pattern.xml
index 023f00bb..7df3d2ca 100644
--- a/src/main/res/layout/pattern.xml
+++ b/src/main/res/layout/pattern.xml
@@ -4,7 +4,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 
-    <org.distorted.patternui.PatternSurfaceView
+    <org.distorted.patterns.PatternSurfaceView
         android:id="@+id/patternSurfaceView"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/src/main/res/layout/play.xml b/src/main/res/layout/play.xml
index e7f38ace..61568a4f 100644
--- a/src/main/res/layout/play.xml
+++ b/src/main/res/layout/play.xml
@@ -4,7 +4,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 
-    <org.distorted.playui.PlayView
+    <org.distorted.play.PlayView
         android:id="@+id/playView"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
diff --git a/src/main/res/layout/solver.xml b/src/main/res/layout/solver.xml
index efd6f70e..6c95bf86 100644
--- a/src/main/res/layout/solver.xml
+++ b/src/main/res/layout/solver.xml
@@ -4,7 +4,7 @@
     android:layout_width="match_parent"
     android:layout_height="match_parent" >
 
-    <org.distorted.solverui.SolverSurfaceView
+    <org.distorted.solvers.SolverSurfaceView
         android:id="@+id/solverSurfaceView"
         android:layout_width="match_parent"
         android:layout_height="match_parent"
