commit 55e6be1d3b688dd0cff7ae1b811692d511e26623
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Tue Jun 29 13:37:02 2021 +0200

    Abstract the part that controls the 'Locked' and 'Back Moves' buttons from the two activities: the main one and the tutorial one.
    This code had been duplicated there.

diff --git a/src/main/java/org/distorted/effects/EffectController.java b/src/main/java/org/distorted/effects/EffectController.java
index a68e5223..873566dc 100644
--- a/src/main/java/org/distorted/effects/EffectController.java
+++ b/src/main/java/org/distorted/effects/EffectController.java
@@ -19,6 +19,7 @@
 
 package org.distorted.effects;
 
+import org.distorted.helpers.MovesFinished;
 import org.distorted.library.message.EffectListener;
 import org.distorted.main.RubikPreRender;
 import org.distorted.objects.TwistyObject;
@@ -27,7 +28,7 @@ import org.distorted.objects.TwistyObject;
 
 public interface EffectController extends EffectListener
   {
-  void addRotation(RubikPreRender.ActionFinishedListener listener, int axis, int rowBitmap, int angle, long duration);
+  void addRotation(MovesFinished listener, int axis, int rowBitmap, int angle, long duration);
   TwistyObject getOldObject();
   TwistyObject getObject();
   int getNumScrambles();
diff --git a/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java b/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
index c82f6a75..296c914a 100644
--- a/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
+++ b/src/main/java/org/distorted/effects/scramble/ScrambleEffect.java
@@ -20,6 +20,7 @@
 package org.distorted.effects.scramble;
 
 import org.distorted.effects.BaseEffect;
+import org.distorted.helpers.MovesFinished;
 import org.distorted.library.effect.Effect;
 import org.distorted.library.main.DistortedEffects;
 import org.distorted.library.main.DistortedScreen;
@@ -34,7 +35,7 @@ import java.util.Random;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public abstract class ScrambleEffect extends BaseEffect implements EffectListener, RubikPreRender.ActionFinishedListener
+public abstract class ScrambleEffect extends BaseEffect implements EffectListener, MovesFinished
 {
   public enum Type
     {
diff --git a/src/main/java/org/distorted/helpers/MovesAndLockController.java b/src/main/java/org/distorted/helpers/MovesAndLockController.java
new file mode 100644
index 00000000..f45dea33
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/MovesAndLockController.java
@@ -0,0 +1,203 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import android.view.View;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+
+import java.util.ArrayList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class MovesAndLockController implements MovesFinished
+  {
+  private static final int MILLIS_PER_DEGREE = 6;
+
+  private static class Move
+    {
+    private final int mAxis, mRow, mAngle;
+
+    Move(int axis, int row, int angle)
+      {
+      mAxis = axis;
+      mRow  = row;
+      mAngle= angle;
+      }
+    }
+
+  private ArrayList<Move> mMoves;
+  private boolean mCanPrevMove;
+  private TwistyPreRender mPre;
+  private ImageButton mPrevButton, mLockButton;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+
+  public MovesAndLockController()
+    {
+    mCanPrevMove = true;
+
+    if( mMoves==null ) mMoves = new ArrayList<>();
+    else               mMoves.clear();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void toggleLock(TwistyActivity act)
+    {
+    act.toggleLock();
+    mLockButton.setImageResource(getLockIcon(act));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public int getLockIcon(TwistyActivity act)
+    {
+    if( act.retLocked() )
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_locked,R.drawable.ui_medium_locked, R.drawable.ui_big_locked, R.drawable.ui_huge_locked);
+      }
+    else
+      {
+      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,R.drawable.ui_medium_unlocked, R.drawable.ui_big_unlocked, R.drawable.ui_huge_unlocked);
+      }
+    }
+
+//////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void backMove(TwistyPreRender pre)
+    {
+    if( mCanPrevMove )
+      {
+      int numMoves = mMoves.size();
+
+      if( numMoves>0 )
+        {
+        Move move   = mMoves.remove(numMoves-1);
+        int axis    = move.mAxis;
+        int row     = (1<<move.mRow);
+        int angle   = move.mAngle;
+        int duration= Math.abs(angle)*MILLIS_PER_DEGREE;
+
+        if( angle!=0 )
+          {
+          mCanPrevMove = false;
+          mPre = pre;
+          pre.blockTouch();
+          pre.addRotation(this, axis, row, -angle, duration);
+          }
+        else
+          {
+          android.util.Log.e("solution", "error: trying to back move of angle 0");
+          }
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addMove(int axis, int row, int angle)
+    {
+    mMoves.add(new Move(axis,row,angle));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void onActionFinished(final long effectID)
+    {
+    mCanPrevMove = true;
+    mPre.unblockTouch();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void clearMoves()
+    {
+    mMoves.clear();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setupPrevButton(final TwistyActivity act, final float width)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_back,R.drawable.ui_medium_cube_back, R.drawable.ui_big_cube_back, R.drawable.ui_huge_cube_back);
+    mPrevButton = new TransparentImageButton(act, icon, width, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        TwistyPreRender pre = act.getTwistyPreRender();
+        backMove(pre);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setupLockButton(final TwistyActivity act, final float width)
+    {
+    final int icon = getLockIcon(act);
+    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
+
+    mLockButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        toggleLock(act);
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setLockState(final TwistyActivity act)
+    {
+    act.runOnUiThread(new Runnable()
+      {
+      @Override
+      public void run()
+        {
+        if( mLockButton!=null )
+          mLockButton.setImageResource(getLockIcon(act));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ImageButton getPrevButton()
+    {
+    return mPrevButton;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public ImageButton getLockButton()
+    {
+    return mLockButton;
+    }
+  }
diff --git a/src/main/java/org/distorted/helpers/MovesFinished.java b/src/main/java/org/distorted/helpers/MovesFinished.java
new file mode 100644
index 00000000..e6bbb7c0
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/MovesFinished.java
@@ -0,0 +1,27 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public interface MovesFinished
+  {
+  void onActionFinished(long effectID);
+  }
diff --git a/src/main/java/org/distorted/helpers/TransparentButton.java b/src/main/java/org/distorted/helpers/TransparentButton.java
new file mode 100644
index 00000000..1aec98d6
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/TransparentButton.java
@@ -0,0 +1,56 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.TypedValue;
+import android.widget.LinearLayout;
+
+import org.distorted.main.RubikActivity;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+@SuppressLint("ViewConstructor")
+public class TransparentButton extends androidx.appcompat.widget.AppCompatButton
+{
+   public TransparentButton(Context context, int resId, float size, float scrWidth)
+      {
+      super(context);
+
+      final int padding = (int)(scrWidth*RubikActivity.PADDING);
+      final int margin  = (int)(scrWidth*RubikActivity.MARGIN);
+
+      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT, 1.0f);
+      params.topMargin    = margin;
+      params.bottomMargin = margin;
+      params.leftMargin   = margin;
+      params.rightMargin  = margin;
+
+      setLayoutParams(params);
+      setPadding(padding,0,padding,0);
+      setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
+      setText(resId);
+
+      TypedValue outValue = new TypedValue();
+      context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true);
+      setBackgroundResource(outValue.resourceId);
+      }
+}
diff --git a/src/main/java/org/distorted/helpers/TransparentImageButton.java b/src/main/java/org/distorted/helpers/TransparentImageButton.java
new file mode 100644
index 00000000..fd7348a6
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/TransparentImageButton.java
@@ -0,0 +1,56 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import android.annotation.SuppressLint;
+import android.content.Context;
+import android.util.TypedValue;
+import android.widget.LinearLayout;
+
+import org.distorted.main.RubikActivity;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+@SuppressLint("ViewConstructor")
+public class TransparentImageButton extends androidx.appcompat.widget.AppCompatImageButton
+{
+  public TransparentImageButton(Context context, int icon, float scrWidth, int butWidth)
+      {
+      super(context);
+
+      final int padding = (int)(scrWidth*RubikActivity.PADDING);
+      final int margin  = (int)(scrWidth*RubikActivity.MARGIN);
+
+      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(butWidth,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+
+      params.topMargin    = margin;
+      params.bottomMargin = margin;
+      params.leftMargin   = margin;
+      params.rightMargin  = margin;
+
+      setLayoutParams(params);
+      setPadding(padding,0,padding,0);
+      setImageResource(icon);
+
+      TypedValue outValue = new TypedValue();
+      context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true);
+      setBackgroundResource(outValue.resourceId);
+      }
+}
diff --git a/src/main/java/org/distorted/helpers/TwistyActivity.java b/src/main/java/org/distorted/helpers/TwistyActivity.java
new file mode 100644
index 00000000..16773f5f
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/TwistyActivity.java
@@ -0,0 +1,85 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import org.distorted.screens.ScreenList;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+abstract public class TwistyActivity extends AppCompatActivity
+  {
+  boolean mIsLocked, mRemLocked;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public abstract TwistyPreRender getTwistyPreRender();
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean retLocked()
+      {
+      return mIsLocked;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void toggleLock()
+      {
+      mIsLocked = !mIsLocked;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void unlock()
+    {
+    mIsLocked = false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isLocked()
+    {
+    ScreenList state = ScreenList.getCurrentScreen();
+
+    if( state== ScreenList.PLAY || state== ScreenList.READ || state== ScreenList.SOLV )
+      {
+      return mIsLocked;
+      }
+
+    return false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void setLock()
+    {
+    mRemLocked = mIsLocked;
+    mIsLocked = true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void unsetLock()
+    {
+    mIsLocked = mRemLocked;
+    }
+  }
diff --git a/src/main/java/org/distorted/helpers/TwistyPreRender.java b/src/main/java/org/distorted/helpers/TwistyPreRender.java
new file mode 100644
index 00000000..117473cd
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/TwistyPreRender.java
@@ -0,0 +1,32 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is free software: you can redistribute it and/or modify                            //
+// it under the terms of the GNU General Public License as published by                          //
+// the Free Software Foundation, either version 2 of the License, or                             //
+// (at your option) any later version.                                                           //
+//                                                                                               //
+// Magic Cube is distributed in the hope that it will be useful,                                 //
+// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
+// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
+// GNU General Public License for more details.                                                  //
+//                                                                                               //
+// You should have received a copy of the GNU General Public License                             //
+// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.helpers;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public interface TwistyPreRender
+  {
+  void blockTouch();
+  void unblockTouch();
+  void blockEverything();
+  void unblockEverything();
+  void addRotation(MovesFinished listener, int axis, int rowBitmap, int angle, long duration);
+  void solveObject();
+  }
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index 4d017c33..9035fb5f 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -25,7 +25,6 @@ import android.os.Build;
 import android.os.Bundle;
 import android.os.LocaleList;
 import android.preference.PreferenceManager;
-import androidx.appcompat.app.AppCompatActivity;
 
 import android.util.DisplayMetrics;
 import android.view.DisplayCutout;
@@ -39,6 +38,8 @@ import com.google.firebase.analytics.FirebaseAnalytics;
 import org.distorted.dialogs.RubikDialogError;
 import org.distorted.dialogs.RubikDialogPrivacy;
 import org.distorted.effects.BaseEffect;
+import org.distorted.helpers.TwistyActivity;
+import org.distorted.helpers.TwistyPreRender;
 import org.distorted.library.main.DistortedLibrary;
 
 import org.distorted.library.main.DistortedScreen;
@@ -54,7 +55,7 @@ import java.util.Locale;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class RubikActivity extends AppCompatActivity
+public class RubikActivity extends TwistyActivity
 {
     public static final float PADDING             = 0.01f;
     public static final float MARGIN              = 0.004f;
@@ -88,7 +89,6 @@ public class RubikActivity extends AppCompatActivity
     private static int mScreenWidth, mScreenHeight;
     private boolean mPolicyAccepted, mIsChinese;
     private int mCurrentApiVersion;
-    private boolean mIsLocked, mRemLocked;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -110,7 +110,7 @@ public class RubikActivity extends AppCompatActivity
       mScreenHeight=displaymetrics.heightPixels;
 
       mIsChinese = localeIsChinese();
-      mIsLocked = false;
+      unlock();
 
       hideNavigationBar();
       cutoutHack();
@@ -402,6 +402,14 @@ public class RubikActivity extends AppCompatActivity
       return view.getPreRender();
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyPreRender getTwistyPreRender()
+      {
+      RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
+      return view.getPreRender();
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public RubikSurfaceView getSurfaceView()
@@ -561,35 +569,13 @@ public class RubikActivity extends AppCompatActivity
       return view.isVertical();
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void toggleLock()
-      {
-      mIsLocked = !mIsLocked;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean isLocked()
-      {
-      ScreenList state = ScreenList.getCurrentScreen();
-
-      if( state== ScreenList.PLAY || state== ScreenList.READ || state== ScreenList.SOLV )
-        {
-        return mIsLocked;
-        }
-
-      return false;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void blockEverything()
       {
-      mRemLocked = mIsLocked;
-      mIsLocked = true;
+      setLock();
 
-      RubikPreRender pre = getPreRender();
+      TwistyPreRender pre = getPreRender();
       pre.blockEverything();
 
       RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
@@ -600,22 +586,15 @@ public class RubikActivity extends AppCompatActivity
 
     public void unblockEverything()
       {
-      mIsLocked = mRemLocked;
+      unsetLock();
 
-      RubikPreRender pre = getPreRender();
+      TwistyPreRender pre = getPreRender();
       pre.unblockEverything();
 
       RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
       play.setLockState(this);
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean retLocked()
-      {
-      return mIsLocked;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void switchTutorial(String url, ObjectList object, int size)
diff --git a/src/main/java/org/distorted/main/RubikPreRender.java b/src/main/java/org/distorted/main/RubikPreRender.java
index 06441370..55c14b51 100644
--- a/src/main/java/org/distorted/main/RubikPreRender.java
+++ b/src/main/java/org/distorted/main/RubikPreRender.java
@@ -39,6 +39,8 @@ import org.distorted.dialogs.RubikDialogSolved;
 import org.distorted.effects.BaseEffect;
 import org.distorted.effects.EffectController;
 import org.distorted.effects.scramble.ScrambleEffect;
+import org.distorted.helpers.MovesFinished;
+import org.distorted.helpers.TwistyPreRender;
 import org.distorted.objects.TwistyObject;
 import org.distorted.objects.ObjectList;
 import org.distorted.network.RubikScores;
@@ -48,13 +50,8 @@ import org.distorted.screens.RubikScreenSolving;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class RubikPreRender implements EffectController
+public class RubikPreRender implements EffectController, TwistyPreRender
   {
-  public interface ActionFinishedListener
-    {
-    void onActionFinished(long effectID);
-    }
-
   private final RubikSurfaceView mView;
   private boolean mFinishRotation, mRemoveRotation, mRemovePatternRotation, mAddRotation,
                   mSetQuat, mChangeObject, mSetupObject, mSolveObject, mScrambleObject,
@@ -74,7 +71,7 @@ public class RubikPreRender implements EffectController
   private int mScrambleObjectNum;
   private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
   private long mAddRotationDuration;
-  private ActionFinishedListener mAddActionListener;
+  private MovesFinished mAddActionListener;
   private long mAddRotationID, mRemoveRotationID;
   private int mCubit, mFace, mNewColor;
   private int mNearestAngle;
@@ -602,7 +599,7 @@ public class RubikPreRender implements EffectController
 // PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void addRotation(ActionFinishedListener listener, int axis, int rowBitmap, int angle, long duration)
+  public void addRotation(MovesFinished listener, int axis, int rowBitmap, int angle, long duration)
     {
     mAddRotation = true;
 
diff --git a/src/main/java/org/distorted/patterns/RubikPattern.java b/src/main/java/org/distorted/patterns/RubikPattern.java
index d185404f..790f9749 100644
--- a/src/main/java/org/distorted/patterns/RubikPattern.java
+++ b/src/main/java/org/distorted/patterns/RubikPattern.java
@@ -19,6 +19,7 @@
 
 package org.distorted.patterns;
 
+import org.distorted.helpers.MovesFinished;
 import org.distorted.main.RubikPreRender;
 
 import java.util.ArrayList;
@@ -192,7 +193,7 @@ public class RubikPattern
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private static class Pattern implements RubikPreRender.ActionFinishedListener
+  private static class Pattern implements MovesFinished
     {
     private final String nameStr;
     private String moveStr;
diff --git a/src/main/java/org/distorted/screens/RubikScreenBase.java b/src/main/java/org/distorted/screens/RubikScreenBase.java
index 467493dc..ba414a67 100644
--- a/src/main/java/org/distorted/screens/RubikScreenBase.java
+++ b/src/main/java/org/distorted/screens/RubikScreenBase.java
@@ -19,101 +19,24 @@
 
 package org.distorted.screens;
 
-import android.view.View;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 
+import org.distorted.helpers.MovesAndLockController;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.main.RubikPreRender;
-
-import java.util.ArrayList;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-abstract class RubikScreenBase extends RubikScreenAbstract implements RubikPreRender.ActionFinishedListener
+abstract class RubikScreenBase extends RubikScreenAbstract
   {
-  private static final int MILLIS_PER_DEGREE = 6;
-
-  private ImageButton mPrevButton, mLockButton;
-  private boolean mCanPrevMove;
-  private RubikPreRender mPre;
-
-  private static class Move
-    {
-    private final int mAxis, mRow, mAngle;
-
-    Move(int axis, int row, int angle)
-      {
-      mAxis = axis;
-      mRow  = row;
-      mAngle= angle;
-      }
-    }
-
-  ArrayList<Move> mMoves;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void backMove(RubikPreRender pre)
-    {
-    if( mCanPrevMove )
-      {
-      int numMoves = mMoves.size();
-
-      if( numMoves>0 )
-        {
-        RubikScreenBase.Move move = mMoves.remove(numMoves-1);
-        int axis  = move.mAxis;
-        int row   = (1<<move.mRow);
-        int angle = move.mAngle;
-        int duration= Math.abs(angle)*MILLIS_PER_DEGREE;
-
-        if( angle!=0 )
-          {
-          mCanPrevMove = false;
-          mPre = pre;
-          pre.blockTouch();
-          pre.addRotation(this, axis, row, -angle, duration);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: trying to back move of angle 0");
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void toggleLock(RubikActivity act)
-    {
-    act.toggleLock();
-    mLockButton.setImageResource(getLockIcon(act));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getLockIcon(RubikActivity act)
-    {
-    if( act.retLocked() )
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_locked,R.drawable.ui_medium_locked, R.drawable.ui_big_locked, R.drawable.ui_huge_locked);
-      }
-    else
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,R.drawable.ui_medium_unlocked, R.drawable.ui_big_unlocked, R.drawable.ui_huge_unlocked);
-      }
-    }
+  MovesAndLockController mController;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   void createBottomPane(final RubikActivity act, float width, ImageButton button)
     {
-    mCanPrevMove = true;
-
-    if( mMoves==null ) mMoves = new ArrayList<>();
-    else               mMoves.clear();
+    if( mController==null ) mController = new MovesAndLockController();
 
     LinearLayout layoutBot = act.findViewById(R.id.lowerBar);
     layoutBot.removeAllViews();
@@ -127,10 +50,10 @@ abstract class RubikScreenBase extends RubikScreenAbstract implements RubikPreRe
     LinearLayout layoutRight = new LinearLayout(act);
     layoutRight.setLayoutParams(params);
 
-    setupPrevButton(act,width);
-    layoutLeft.addView(mPrevButton);
-    setupLockButton(act,width);
-    layoutMid.addView(mLockButton);
+    mController.setupPrevButton(act,width);
+    layoutLeft.addView(mController.getPrevButton());
+    mController.setupLockButton(act,width);
+    layoutMid.addView(mController.getLockButton());
     layoutRight.addView(button);
 
     layoutBot.addView(layoutLeft);
@@ -138,68 +61,17 @@ abstract class RubikScreenBase extends RubikScreenAbstract implements RubikPreRe
     layoutBot.addView(layoutRight);
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setupLockButton(final RubikActivity act, final float width)
-    {
-    final int icon = getLockIcon(act);
-    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mLockButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        toggleLock(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  void setupPrevButton(final RubikActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_back,R.drawable.ui_medium_cube_back, R.drawable.ui_big_cube_back, R.drawable.ui_huge_cube_back);
-    mPrevButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        RubikPreRender pre = act.getPreRender();
-        backMove(pre);
-        }
-      });
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void setLockState(final RubikActivity act)
     {
-    act.runOnUiThread(new Runnable()
-      {
-      @Override
-      public void run()
-        {
-        if( mLockButton!=null )
-          mLockButton.setImageResource(getLockIcon(act));
-        }
-      });
+    mController.setLockState(act);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public void addMove(int axis, int row, int angle)
     {
-    mMoves.add(new Move(axis,row,angle));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onActionFinished(final long effectID)
-    {
-    mCanPrevMove = true;
-    mPre.unblockTouch();
+    mController.addMove(axis,row,angle);
     }
   }
diff --git a/src/main/java/org/distorted/screens/RubikScreenDone.java b/src/main/java/org/distorted/screens/RubikScreenDone.java
index e1c838f4..565d1f2e 100644
--- a/src/main/java/org/distorted/screens/RubikScreenDone.java
+++ b/src/main/java/org/distorted/screens/RubikScreenDone.java
@@ -31,6 +31,7 @@ import androidx.fragment.app.FragmentManager;
 
 import org.distorted.dialogs.RubikDialogNewRecord;
 import org.distorted.dialogs.RubikDialogSolved;
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenPattern.java b/src/main/java/org/distorted/screens/RubikScreenPattern.java
index 8475757b..024067c2 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPattern.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPattern.java
@@ -31,6 +31,7 @@ import android.widget.LinearLayout;
 import android.widget.TextView;
 
 import org.distorted.dialogs.RubikDialogPattern;
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
diff --git a/src/main/java/org/distorted/screens/RubikScreenPlay.java b/src/main/java/org/distorted/screens/RubikScreenPlay.java
index cc81637f..5b4fab1f 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPlay.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPlay.java
@@ -37,6 +37,8 @@ import org.distorted.dialogs.RubikDialogAbout;
 import org.distorted.dialogs.RubikDialogPattern;
 import org.distorted.dialogs.RubikDialogScores;
 import org.distorted.dialogs.RubikDialogTutorial;
+import org.distorted.helpers.TransparentButton;
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
@@ -291,7 +293,7 @@ public class RubikScreenPlay extends RubikScreenBase
               mSize   = sizes[index];
               act.changeObject(list,sizes[index], true);
               adjustLevels(act);
-              mMoves.clear();
+              mController.clearMoves();
               }
 
             mObjectPopup.dismiss();
@@ -426,7 +428,7 @@ public class RubikScreenPlay extends RubikScreenBase
       public void onClick(View v)
         {
         act.getPreRender().solveObject();
-        mMoves.clear();
+        mController.clearMoves();
         }
       });
     }
diff --git a/src/main/java/org/distorted/screens/RubikScreenReady.java b/src/main/java/org/distorted/screens/RubikScreenReady.java
index 61fa1c06..08091fe4 100644
--- a/src/main/java/org/distorted/screens/RubikScreenReady.java
+++ b/src/main/java/org/distorted/screens/RubikScreenReady.java
@@ -27,6 +27,7 @@ import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolution.java b/src/main/java/org/distorted/screens/RubikScreenSolution.java
index 5fc2ccd9..f4a704d6 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolution.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolution.java
@@ -28,6 +28,8 @@ import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import org.distorted.helpers.MovesFinished;
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
@@ -36,7 +38,7 @@ import org.distorted.patterns.RubikPattern;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class RubikScreenSolution extends RubikScreenAbstract implements RubikPreRender.ActionFinishedListener
+public class RubikScreenSolution extends RubikScreenAbstract implements MovesFinished
   {
   private static final int MILLIS_PER_DEGREE = 6;
 
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolver.java b/src/main/java/org/distorted/screens/RubikScreenSolver.java
index 8ebb101d..9e607a38 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolver.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolver.java
@@ -32,6 +32,7 @@ import android.widget.ImageButton;
 import android.widget.LinearLayout;
 
 import org.distorted.dialogs.RubikDialogSolverError;
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.main.RubikPreRender;
diff --git a/src/main/java/org/distorted/screens/RubikScreenSolving.java b/src/main/java/org/distorted/screens/RubikScreenSolving.java
index 07df9282..b24457f0 100644
--- a/src/main/java/org/distorted/screens/RubikScreenSolving.java
+++ b/src/main/java/org/distorted/screens/RubikScreenSolving.java
@@ -27,6 +27,7 @@ import android.widget.ImageButton;
 import android.widget.LinearLayout;
 import android.widget.TextView;
 
+import org.distorted.helpers.TransparentImageButton;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
 import org.distorted.objects.ObjectList;
diff --git a/src/main/java/org/distorted/screens/TransparentButton.java b/src/main/java/org/distorted/screens/TransparentButton.java
deleted file mode 100644
index 62df6524..00000000
--- a/src/main/java/org/distorted/screens/TransparentButton.java
+++ /dev/null
@@ -1,56 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.screens;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.util.TypedValue;
-import android.widget.LinearLayout;
-
-import org.distorted.main.RubikActivity;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-@SuppressLint("ViewConstructor")
-public class TransparentButton extends androidx.appcompat.widget.AppCompatButton
-{
-   public TransparentButton(Context context, int resId, float size, float scrWidth)
-      {
-      super(context);
-
-      final int padding = (int)(scrWidth*RubikActivity.PADDING);
-      final int margin  = (int)(scrWidth*RubikActivity.MARGIN);
-
-      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT, 1.0f);
-      params.topMargin    = margin;
-      params.bottomMargin = margin;
-      params.leftMargin   = margin;
-      params.rightMargin  = margin;
-
-      setLayoutParams(params);
-      setPadding(padding,0,padding,0);
-      setTextSize(TypedValue.COMPLEX_UNIT_PX, size);
-      setText(resId);
-
-      TypedValue outValue = new TypedValue();
-      context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true);
-      setBackgroundResource(outValue.resourceId);
-      }
-}
diff --git a/src/main/java/org/distorted/screens/TransparentImageButton.java b/src/main/java/org/distorted/screens/TransparentImageButton.java
deleted file mode 100644
index 5a4e4f52..00000000
--- a/src/main/java/org/distorted/screens/TransparentImageButton.java
+++ /dev/null
@@ -1,56 +0,0 @@
-///////////////////////////////////////////////////////////////////////////////////////////////////
-// Copyright 2020 Leszek Koltunski                                                               //
-//                                                                                               //
-// This file is part of Magic Cube.                                                              //
-//                                                                                               //
-// Magic Cube is free software: you can redistribute it and/or modify                            //
-// it under the terms of the GNU General Public License as published by                          //
-// the Free Software Foundation, either version 2 of the License, or                             //
-// (at your option) any later version.                                                           //
-//                                                                                               //
-// Magic Cube is distributed in the hope that it will be useful,                                 //
-// but WITHOUT ANY WARRANTY; without even the implied warranty of                                //
-// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the                                 //
-// GNU General Public License for more details.                                                  //
-//                                                                                               //
-// You should have received a copy of the GNU General Public License                             //
-// along with Magic Cube.  If not, see <http://www.gnu.org/licenses/>.                           //
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-package org.distorted.screens;
-
-import android.annotation.SuppressLint;
-import android.content.Context;
-import android.util.TypedValue;
-import android.widget.LinearLayout;
-
-import org.distorted.main.RubikActivity;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-@SuppressLint("ViewConstructor")
-public class TransparentImageButton extends androidx.appcompat.widget.AppCompatImageButton
-{
-  public TransparentImageButton(Context context, int icon, float scrWidth, int butWidth)
-      {
-      super(context);
-
-      final int padding = (int)(scrWidth*RubikActivity.PADDING);
-      final int margin  = (int)(scrWidth*RubikActivity.MARGIN);
-
-      LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(butWidth,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
-
-      params.topMargin    = margin;
-      params.bottomMargin = margin;
-      params.leftMargin   = margin;
-      params.rightMargin  = margin;
-
-      setLayoutParams(params);
-      setPadding(padding,0,padding,0);
-      setImageResource(icon);
-
-      TypedValue outValue = new TypedValue();
-      context.getTheme().resolveAttribute(android.R.attr.selectableItemBackgroundBorderless, outValue, true);
-      setBackgroundResource(outValue.resourceId);
-      }
-}
diff --git a/src/main/java/org/distorted/tutorials/TutorialActivity.java b/src/main/java/org/distorted/tutorials/TutorialActivity.java
index 57409e3c..ea1c1ec3 100644
--- a/src/main/java/org/distorted/tutorials/TutorialActivity.java
+++ b/src/main/java/org/distorted/tutorials/TutorialActivity.java
@@ -28,22 +28,21 @@ import android.view.WindowManager;
 import android.webkit.WebView;
 import android.widget.LinearLayout;
 
-import androidx.appcompat.app.AppCompatActivity;
-
 import com.google.firebase.analytics.FirebaseAnalytics;
 
 import org.distorted.dialogs.RubikDialogError;
+import org.distorted.helpers.TwistyActivity;
+import org.distorted.helpers.TwistyPreRender;
 import org.distorted.library.main.DistortedLibrary;
 import org.distorted.main.R;
 import org.distorted.objects.ObjectList;
 import org.distorted.objects.TwistyObject;
-import org.distorted.screens.ScreenList;
 
 import static org.distorted.main.RubikRenderer.BRIGHTNESS;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class TutorialActivity extends AppCompatActivity
+public class TutorialActivity extends TwistyActivity
 {
     private static final String URL = "https://www.youtube.com/embed/";
 
@@ -56,10 +55,6 @@ public class TutorialActivity extends AppCompatActivity
                                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
 
-    public static final int FLAGS2=  View.SYSTEM_UI_FLAG_LAYOUT_STABLE
-                                   | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
-
-    private boolean mIsLocked;
     private FirebaseAnalytics mFirebaseAnalytics;
     private static int mScreenWidth, mScreenHeight;
     private int mCurrentApiVersion;
@@ -87,7 +82,7 @@ public class TutorialActivity extends AppCompatActivity
         mObjectSize    = b.getInt("siz");
         }
 
-      mIsLocked = false;
+      unlock();
       mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
 
       DisplayMetrics displaymetrics = new DisplayMetrics();
@@ -153,7 +148,7 @@ public class TutorialActivity extends AppCompatActivity
       mState.createRightPane(this,width);
 
       WebView videoView = findViewById(R.id.tutorialVideoView);
-      mWebView = new TutorialWebView(this,videoView);
+      mWebView = new TutorialWebView(videoView);
       mWebView.load(URL+mURL);
       }
 
@@ -285,6 +280,14 @@ public class TutorialActivity extends AppCompatActivity
       return view.getPreRender();
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public TwistyPreRender getTwistyPreRender()
+      {
+      TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
+      return view.getPreRender();
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public static int getDrawableSize()
@@ -327,32 +330,4 @@ public class TutorialActivity extends AppCompatActivity
       TutorialSurfaceView view = findViewById(R.id.tutorialSurfaceView);
       return view.isVertical();
       }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public void toggleLock()
-      {
-      mIsLocked = !mIsLocked;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean isLocked()
-      {
-      ScreenList state = ScreenList.getCurrentScreen();
-
-      if( state== ScreenList.PLAY || state== ScreenList.READ || state== ScreenList.SOLV )
-        {
-        return mIsLocked;
-        }
-
-      return false;
-      }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public boolean retLocked()
-      {
-      return mIsLocked;
-      }
 }
diff --git a/src/main/java/org/distorted/tutorials/TutorialPreRender.java b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
index 93e4f57a..c91b3e4b 100644
--- a/src/main/java/org/distorted/tutorials/TutorialPreRender.java
+++ b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
@@ -24,27 +24,26 @@ import android.content.res.Resources;
 
 import org.distorted.effects.BaseEffect;
 import org.distorted.effects.EffectController;
+import org.distorted.helpers.MovesFinished;
+import org.distorted.helpers.TwistyPreRender;
 import org.distorted.objects.ObjectList;
 import org.distorted.objects.TwistyObject;
-import org.distorted.main.RubikPreRender.ActionFinishedListener;
-import org.distorted.network.RubikScores;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class TutorialPreRender implements EffectController
+public class TutorialPreRender implements EffectController, TwistyPreRender
   {
-  private ActionFinishedListener mAddActionListener;
+  private MovesFinished mAddActionListener;
   private final TutorialSurfaceView mView;
   private boolean mFinishRotation, mRemoveRotation, mAddRotation,
                   mSetQuat, mChangeObject, mSetupObject, mSolveObject, mScrambleObject,
                   mInitializeObject, mResetAllTextureMaps, mRemovePatternRotation;
-  private boolean mCanPlay, mCanRotate;
+  private boolean mUIBlocked, mTouchBlocked;
   private boolean mIsSolved;
   private ObjectList mNextObject;
   private int mNextSize;
   private long mRotationFinishedID;
   private int mScreenWidth;
-  private int[][] mNextMoves;
   private TwistyObject mOldObject, mNewObject;
   private int mAddRotationAxis, mAddRotationRowBitmap, mAddRotationAngle;
   private long mAddRotationDuration;
@@ -67,8 +66,8 @@ public class TutorialPreRender implements EffectController
     mSolveObject    = false;
     mScrambleObject = false;
 
-    mCanRotate      = true;
-    mCanPlay        = true;
+    unblockEverything();
+
     mOldObject      = null;
     mNewObject      = null;
 
@@ -80,7 +79,7 @@ public class TutorialPreRender implements EffectController
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void createObjectNow(ObjectList object, int size, int[][] moves)
+  private void createObjectNow(ObjectList object, int size)
     {
     if( mOldObject!=null ) mOldObject.releaseResources();
     mOldObject = mNewObject;
@@ -88,7 +87,7 @@ public class TutorialPreRender implements EffectController
     Context con = mView.getContext();
     Resources res = con.getResources();
 
-    mNewObject = object.create(size, mView.getQuat(), moves, res, mScreenWidth);
+    mNewObject = object.create(size, mView.getQuat(), null, res, mScreenWidth);
 
     if( mNewObject!=null )
       {
@@ -114,8 +113,8 @@ public class TutorialPreRender implements EffectController
       }
     catch( Exception ex )
       {
-      mCanPlay  = true;
-      mCanRotate= true;
+      android.util.Log.e("renderer", "exception starting effect: "+ex.getMessage());
+      unblockEverything();
       }
     }
 
@@ -143,15 +142,8 @@ public class TutorialPreRender implements EffectController
     mNewObject.removeRotationNow();
 
     boolean solved = mNewObject.isSolved();
-
-    if( solved && !mIsSolved )
-      {
-      doEffectNow( BaseEffect.Type.WIN );
-      }
-    else
-      {
-      mCanPlay = true;
-      }
+    unblockEverything();
+    if( solved && !mIsSolved ) doEffectNow( BaseEffect.Type.WIN );
 
     mIsSolved = solved;
     }
@@ -173,8 +165,7 @@ public class TutorialPreRender implements EffectController
 
     if( mAddRotationID==0 ) // failed to add effect - should never happen
       {
-      mCanPlay  = true;
-      mCanRotate= true;
+      unblockEverything();
       }
     }
 
@@ -183,12 +174,12 @@ public class TutorialPreRender implements EffectController
   private void finishRotationNow()
     {
     mFinishRotation = false;
-    mCanPlay        = false;
+    blockEverything();
     mRotationFinishedID = mNewObject.finishRotationNow(this, mNearestAngle);
 
     if( mRotationFinishedID==0 ) // failed to add effect - should never happen
       {
-      mCanPlay = true;
+      unblockEverything();
       }
     }
 
@@ -200,8 +191,8 @@ public class TutorialPreRender implements EffectController
 
     if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
       {
-      mCanPlay  = false;
-      createObjectNow(mNextObject, mNextSize, null);
+      blockEverything();
+      createObjectNow(mNextObject, mNextSize);
       doEffectNow( BaseEffect.Type.SIZECHANGE );
       }
     }
@@ -214,35 +205,33 @@ public class TutorialPreRender implements EffectController
 
     if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
       {
-      mCanPlay  = false;
-      createObjectNow(mNextObject, mNextSize, mNextMoves);
+      blockEverything();
+      createObjectNow(mNextObject, mNextSize);
       doEffectNow( BaseEffect.Type.SIZECHANGE );
       }
     else
       {
-      mNewObject.initializeObject(mNextMoves);
+      mNewObject.initializeObject(null);
       }
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void solveObjectNow()
+  private void scrambleObjectNow()
     {
-    mSolveObject = false;
-    mCanPlay     = false;
-    doEffectNow( BaseEffect.Type.SOLVE );
+    mScrambleObject = false;
+    mIsSolved       = false;
+    blockEverything();
+    doEffectNow( BaseEffect.Type.SCRAMBLE );
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  private void scrambleObjectNow()
+  private void solveObjectNow()
     {
-    mScrambleObject = false;
-    mCanPlay        = false;
-    mCanRotate      = false;
-    mIsSolved       = false;
-
-    doEffectNow( BaseEffect.Type.SCRAMBLE );
+    mSolveObject = false;
+    blockEverything();
+    doEffectNow( BaseEffect.Type.SOLVE );
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -250,7 +239,7 @@ public class TutorialPreRender implements EffectController
   private void initializeObjectNow()
     {
     mInitializeObject = false;
-    mNewObject.initializeObject(mNextMoves);
+    mNewObject.initializeObject(null);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -333,7 +322,51 @@ public class TutorialPreRender implements EffectController
 // PUBLIC API
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void addRotation(ActionFinishedListener listener, int axis, int rowBitmap, int angle, long duration)
+  boolean isTouchBlocked()
+    {
+    return mTouchBlocked;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public boolean isUINotBlocked()
+    {
+    return !mUIBlocked;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void blockEverything()
+    {
+    mUIBlocked   = true;
+    mTouchBlocked= true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void blockTouch()
+    {
+    mTouchBlocked= true;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void unblockEverything()
+    {
+    mUIBlocked   = false;
+    mTouchBlocked= false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void unblockTouch()
+    {
+    mTouchBlocked= false;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void addRotation(MovesFinished listener, int axis, int rowBitmap, int angle, long duration)
     {
     mAddRotation = true;
 
@@ -344,13 +377,6 @@ public class TutorialPreRender implements EffectController
     mAddRotationDuration  = duration;
     }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  boolean canRotate()
-    {
-    return mCanRotate;
-    }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
   public int getNumScrambles()
@@ -362,7 +388,7 @@ public class TutorialPreRender implements EffectController
 
   public void solveObject()
     {
-    if( mCanPlay )
+    if( !mUIBlocked )
       {
       mSolveObject = true;
       }
@@ -372,7 +398,7 @@ public class TutorialPreRender implements EffectController
 
   public void scrambleObject(int num)
     {
-    if( mCanPlay )
+    if( !mUIBlocked )
       {
       mScrambleObject = true;
       mScrambleObjectNum = num;
@@ -417,8 +443,7 @@ public class TutorialPreRender implements EffectController
       }
     else
       {
-      mCanPlay   = true;
-      mCanRotate = true;
+      unblockEverything();  // buggy? I think we shouldn't do it if the effect is of type 'WIN'
       }
     }
   }
diff --git a/src/main/java/org/distorted/tutorials/TutorialState.java b/src/main/java/org/distorted/tutorials/TutorialState.java
index df6c4294..a2df7216 100644
--- a/src/main/java/org/distorted/tutorials/TutorialState.java
+++ b/src/main/java/org/distorted/tutorials/TutorialState.java
@@ -23,90 +23,20 @@ import android.view.View;
 import android.widget.ImageButton;
 import android.widget.LinearLayout;
 
+import org.distorted.helpers.MovesAndLockController;
 import org.distorted.main.R;
 import org.distorted.main.RubikActivity;
-import org.distorted.main.RubikPreRender;
 import org.distorted.objects.ObjectList;
 import org.distorted.screens.RubikScreenPlay;
 import org.distorted.screens.ScreenList;
-import org.distorted.screens.TransparentImageButton;
-
-import java.util.ArrayList;
+import org.distorted.helpers.TransparentImageButton;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-public class TutorialState implements RubikPreRender.ActionFinishedListener
+public class TutorialState
 {
-  private static final int MILLIS_PER_DEGREE = 6;
-
-  private ImageButton mPrevButton, mLockButton, mSolveButton, mScrambleButton, mBackButton;
-
-  private boolean mCanPrevMove;
-
-  private static class Move
-    {
-    private final int mAxis, mRow, mAngle;
-
-    Move(int axis, int row, int angle)
-      {
-      mAxis = axis;
-      mRow  = row;
-      mAngle= angle;
-      }
-    }
-
-  ArrayList<Move> mMoves;
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void backMove(TutorialPreRender pre)
-    {
-    if( mCanPrevMove )
-      {
-      int numMoves = mMoves.size();
-
-      if( numMoves>0 )
-        {
-        Move move   = mMoves.remove(numMoves-1);
-        int axis    = move.mAxis;
-        int row     = (1<<move.mRow);
-        int angle   = move.mAngle;
-        int duration= Math.abs(angle)*MILLIS_PER_DEGREE;
-
-        if( angle!=0 )
-          {
-          mCanPrevMove = false;
-          pre.addRotation(this, axis, row, -angle, duration);
-          }
-        else
-          {
-          android.util.Log.e("solution", "error: trying to back move of angle 0");
-          }
-        }
-      }
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void toggleLock(TutorialActivity act)
-    {
-    act.toggleLock();
-    mLockButton.setImageResource(getLockIcon(act));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private int getLockIcon(TutorialActivity act)
-    {
-    if( act.retLocked() )
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_locked,R.drawable.ui_medium_locked, R.drawable.ui_big_locked, R.drawable.ui_huge_locked);
-      }
-    else
-      {
-      return RubikActivity.getDrawable(R.drawable.ui_small_unlocked,R.drawable.ui_medium_unlocked, R.drawable.ui_big_unlocked, R.drawable.ui_huge_unlocked);
-      }
-    }
+  private ImageButton mSolveButton, mScrambleButton, mBackButton;
+  private MovesAndLockController mController;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -121,42 +51,7 @@ public class TutorialState implements RubikPreRender.ActionFinishedListener
       public void onClick(View v)
         {
         act.getPreRender().solveObject();
-        mMoves.clear();
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupLockButton(final TutorialActivity act, final float width)
-    {
-    final int icon = getLockIcon(act);
-    mLockButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mLockButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        toggleLock(act);
-        }
-      });
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  private void setupPrevButton(final TutorialActivity act, final float width)
-    {
-    int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_back,R.drawable.ui_medium_cube_back, R.drawable.ui_big_cube_back, R.drawable.ui_huge_cube_back);
-    mPrevButton = new TransparentImageButton(act, icon, width,LinearLayout.LayoutParams.MATCH_PARENT);
-
-    mPrevButton.setOnClickListener( new View.OnClickListener()
-      {
-      @Override
-      public void onClick(View v)
-        {
-        TutorialPreRender pre = act.getPreRender();
-        backMove(pre);
+        mController.clearMoves();
         }
       });
     }
@@ -205,38 +100,28 @@ public class TutorialState implements RubikPreRender.ActionFinishedListener
 
   void createRightPane(final TutorialActivity act, float width)
     {
-    mCanPrevMove = true;
-
-    if( mMoves==null ) mMoves = new ArrayList<>();
-    else               mMoves.clear();
+    if( mController==null ) mController = new MovesAndLockController();
 
     LinearLayout layout = act.findViewById(R.id.tutorialRightBar);
     layout.removeAllViews();
 
-    setupPrevButton(act,width);
-    setupLockButton(act,width);
+    mController.setupPrevButton(act,width);
+    mController.setupLockButton(act,width);
     setupSolveButton(act,width);
     setupScrambleButton(act,width);
     setupBackButton(act,width);
 
     layout.addView(mSolveButton);
-    layout.addView(mPrevButton);
+    layout.addView(mController.getPrevButton());
     layout.addView(mScrambleButton);
-    layout.addView(mLockButton);
+    layout.addView(mController.getLockButton());
     layout.addView(mBackButton);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void addMove(int axis, int row, int angle)
-    {
-    mMoves.add(new Move(axis,row,angle));
-    }
-
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-  public void onActionFinished(final long effectID)
+  void addMove(int axis, int row, int angle)
     {
-    mCanPrevMove = true;
+    mController.addMove(axis,row,angle);
     }
 }
diff --git a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
index 9d456ff2..0f4b8197 100644
--- a/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
+++ b/src/main/java/org/distorted/tutorials/TutorialSurfaceView.java
@@ -293,7 +293,7 @@ public class TutorialSurfaceView extends GLSurfaceView
           {
           mDragging           = false;
           mContinuingRotation = false;
-          mBeginningRotation  = mPreRender.canRotate();
+          mBeginningRotation= !mPreRender.isTouchBlocked();
           }
         else
           {
diff --git a/src/main/java/org/distorted/tutorials/TutorialWebView.java b/src/main/java/org/distorted/tutorials/TutorialWebView.java
index 76b5fdc3..d8ba0b81 100644
--- a/src/main/java/org/distorted/tutorials/TutorialWebView.java
+++ b/src/main/java/org/distorted/tutorials/TutorialWebView.java
@@ -20,7 +20,6 @@
 package org.distorted.tutorials;
 
 import android.annotation.SuppressLint;
-import android.content.Context;
 import android.webkit.WebView;
 import android.webkit.WebViewClient;
 
@@ -28,17 +27,15 @@ import android.webkit.WebViewClient;
 
 public class TutorialWebView
 {
-    private String  mUrl;
-    private Context mContext;
-    private WebView mWebView;
+    private String mUrl;
+    private final WebView mWebView;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     @SuppressLint("SetJavaScriptEnabled")
-    public TutorialWebView(Context context, WebView webview)
+    public TutorialWebView(WebView webview)
       {
       mWebView = webview;
-      mContext = context;
       mWebView.setBackgroundColor(0);
       mWebView.getSettings().setJavaScriptEnabled(true);
 
