commit 809c3432bd1ed1fb0cbb2298c152f1433a23a5ee
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Wed Jun 30 18:05:19 2021 +0200

    Introduce a BlockController - a watchdog which makes sure the Touch and UI blocks do not take too long.
    If it detecs a long block, it unblocks and reports the situation to Crashylytics.

diff --git a/src/main/java/org/distorted/control/RubikControl.java b/src/main/java/org/distorted/control/RubikControl.java
index 9059db6c..c91d419e 100644
--- a/src/main/java/org/distorted/control/RubikControl.java
+++ b/src/main/java/org/distorted/control/RubikControl.java
@@ -19,6 +19,7 @@
 
 package org.distorted.control;
 
+import org.distorted.helpers.BlockController;
 import org.distorted.library.main.DistortedNode;
 import org.distorted.library.main.DistortedScreen;
 import org.distorted.library.message.EffectListener;
@@ -174,7 +175,7 @@ public class RubikControl implements EffectListener
 
   public void animateAll(RubikActivity act)
     {
-    act.blockEverything();
+    act.blockEverything(BlockController.CONTROL_PLACE_0);
     mRefAct = new WeakReference<>(act);
 
     mWholeReturned = false;
@@ -187,7 +188,7 @@ public class RubikControl implements EffectListener
 
   public void animateRotate(RubikActivity act)
     {
-    act.blockEverything();
+    act.blockEverything(BlockController.CONTROL_PLACE_1);
     mRefAct = new WeakReference<>(act);
 
     mWholeReturned = true;
diff --git a/src/main/java/org/distorted/helpers/BlockController.java b/src/main/java/org/distorted/helpers/BlockController.java
new file mode 100644
index 00000000..a7ca7a04
--- /dev/null
+++ b/src/main/java/org/distorted/helpers/BlockController.java
@@ -0,0 +1,204 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// 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 com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.main.BuildConfig;
+
+import java.lang.ref.WeakReference;
+import java.util.Timer;
+import java.util.TimerTask;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class BlockController
+  {
+  public static final int RUBIK_PLACE_0 =0;
+  public static final int RUBIK_PLACE_1 =1;
+  public static final int RUBIK_PLACE_2 =2;
+  public static final int RUBIK_PLACE_3 =3;
+  public static final int RUBIK_PLACE_4 =4;
+  public static final int TUTORIAL_PLACE_0 =10;
+  public static final int TUTORIAL_PLACE_1 =11;
+  public static final int TUTORIAL_PLACE_2 =12;
+  public static final int TUTORIAL_PLACE_3 =13;
+  public static final int TUTORIAL_PLACE_4 =14;
+  public static final int CONTROL_PLACE_0 =20;
+  public static final int CONTROL_PLACE_1 =21;
+  public static final int MOVES_PLACE_0 =30;
+
+  private static final long THRESHHOLD_0 =  5000;
+  private static final long THRESHHOLD_1 = 25000;
+
+  private static long mPauseTime, mResumeTime;
+
+  private long mTouchBlockTime, mUIBlockTime;
+  private int mLastTouchPlace, mLastUIPlace;
+
+  private final WeakReference<TwistyActivity> mAct;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void onPause()
+    {
+    mPauseTime = System.currentTimeMillis();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public static void onResume()
+    {
+    mResumeTime = System.currentTimeMillis();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public BlockController(TwistyActivity act)
+    {
+    mAct = new WeakReference<>(act);
+
+    Timer timer = new Timer();
+
+    timer.scheduleAtFixedRate(new TimerTask()
+      {
+      @Override
+      public void run()
+        {
+        act.runOnUiThread(new Runnable()
+          {
+          @Override
+          public void run()
+            {
+            checkingThread();
+            }
+          });
+        }
+      }, 0, 1000);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// RUBIK_PLACE_3 and TUTORIAL_PLACE_3 are scrambles, those can take up to 20 seconds.
+
+  private void checkingThread()
+    {
+    long now = System.currentTimeMillis();
+
+    long touchThreshhold = (mLastTouchPlace==RUBIK_PLACE_3 || mLastTouchPlace==TUTORIAL_PLACE_3) ? THRESHHOLD_1 : THRESHHOLD_0;
+
+    if( mTouchBlockTime>mPauseTime && now-mTouchBlockTime>touchThreshhold )
+      {
+      TwistyActivity act = mAct.get();
+
+      if( act!=null )
+        {
+        TwistyPreRender pre = act.getTwistyPreRender();
+        if( pre!=null ) pre.unblockTouch();
+        }
+
+      reportTouchProblem(touchThreshhold);
+      }
+
+    long uiThreshhold = (mLastUIPlace==RUBIK_PLACE_3 || mLastUIPlace==TUTORIAL_PLACE_3) ? THRESHHOLD_1 : THRESHHOLD_0;
+
+    if( mUIBlockTime>mPauseTime && now-mUIBlockTime>uiThreshhold )
+      {
+      TwistyActivity act = mAct.get();
+
+      if( act!=null )
+        {
+        TwistyPreRender pre = act.getTwistyPreRender();
+        if( pre!=null ) pre.unblockUI();
+        }
+
+      reportUIProblem(uiThreshhold);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportTouchProblem(long time)
+    {
+    String error = "TOUCH BLOCK "+mLastTouchPlace+" blocked for "+time+" milliseconds!";
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("type"  , "Touch" );
+      crashlytics.setCustomKey("place" , mLastTouchPlace );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportUIProblem(long time)
+    {
+    String error = "UI BLOCK "+mLastUIPlace+" blocked for "+time+" milliseconds!";
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("type"  , "UI" );
+      crashlytics.setCustomKey("place" , mLastUIPlace );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void touchBlocked(int place)
+    {
+    mTouchBlockTime = System.currentTimeMillis();
+    mLastTouchPlace = place;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void uiBlocked(int place)
+    {
+    mUIBlockTime = System.currentTimeMillis();
+    mLastUIPlace = place;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void touchUnblocked()
+    {
+    mTouchBlockTime = 0;
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void uiUnblocked()
+    {
+    mUIBlockTime = 0;
+    }
+  }
diff --git a/src/main/java/org/distorted/helpers/MovesAndLockController.java b/src/main/java/org/distorted/helpers/MovesAndLockController.java
index 5ba9bdea..20230faf 100644
--- a/src/main/java/org/distorted/helpers/MovesAndLockController.java
+++ b/src/main/java/org/distorted/helpers/MovesAndLockController.java
@@ -148,7 +148,7 @@ public class MovesAndLockController implements MovesFinished
           {
           mCanPrevMove = false;
           mPre = act.getTwistyPreRender();
-          mPre.blockTouch();
+          mPre.blockTouch(BlockController.MOVES_PLACE_0);
           mPre.addRotation(this, axis, row, -angle, duration);
           }
         else
diff --git a/src/main/java/org/distorted/helpers/TwistyPreRender.java b/src/main/java/org/distorted/helpers/TwistyPreRender.java
index 117473cd..e4677d0e 100644
--- a/src/main/java/org/distorted/helpers/TwistyPreRender.java
+++ b/src/main/java/org/distorted/helpers/TwistyPreRender.java
@@ -23,10 +23,11 @@ package org.distorted.helpers;
 
 public interface TwistyPreRender
   {
-  void blockTouch();
+  void blockTouch(int place);
   void unblockTouch();
-  void blockEverything();
+  void blockEverything(int place);
   void unblockEverything();
+  void unblockUI();
   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 9035fb5f..bb308192 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -38,6 +38,7 @@ 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.BlockController;
 import org.distorted.helpers.TwistyActivity;
 import org.distorted.helpers.TwistyPreRender;
 import org.distorted.library.main.DistortedLibrary;
@@ -228,6 +229,7 @@ public class RubikActivity extends TwistyActivity
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       view.onPause();
       DistortedLibrary.onPause(0);
+      BlockController.onPause();
       RubikNetwork.onPause();
       savePreferences();
       }
@@ -239,6 +241,7 @@ public class RubikActivity extends TwistyActivity
       {
       super.onResume();
       DistortedLibrary.onResume(0);
+      BlockController.onResume();
       RubikSurfaceView view = findViewById(R.id.rubikSurfaceView);
       view.onResume();
       view.initialize();
@@ -571,12 +574,12 @@ public class RubikActivity extends TwistyActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-    public void blockEverything()
+    public void blockEverything(int place)
       {
       setLock();
 
       TwistyPreRender pre = getPreRender();
-      pre.blockEverything();
+      pre.blockEverything(place);
 
       RubikScreenPlay play = (RubikScreenPlay) ScreenList.PLAY.getScreenClass();
       play.setLockState(this);
diff --git a/src/main/java/org/distorted/main/RubikPreRender.java b/src/main/java/org/distorted/main/RubikPreRender.java
index 55c14b51..ef969350 100644
--- a/src/main/java/org/distorted/main/RubikPreRender.java
+++ b/src/main/java/org/distorted/main/RubikPreRender.java
@@ -39,6 +39,7 @@ 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.BlockController;
 import org.distorted.helpers.MovesFinished;
 import org.distorted.helpers.TwistyPreRender;
 import org.distorted.objects.TwistyObject;
@@ -77,6 +78,7 @@ public class RubikPreRender implements EffectController, TwistyPreRender
   private int mNearestAngle;
   private String mDebug;
   private long mDebugStartTime;
+  private final BlockController mBlockController;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -94,8 +96,6 @@ public class RubikPreRender implements EffectController, TwistyPreRender
     mSolveObject          = false;
     mScrambleObject       = false;
 
-    unblockEverything();
-
     mOldObject = null;
     mNewObject = null;
 
@@ -105,6 +105,10 @@ public class RubikPreRender implements EffectController, TwistyPreRender
     mEffectID = new long[BaseEffect.Type.LENGTH];
 
     mDebug = "";
+
+    RubikActivity act = (RubikActivity)mView.getContext();
+    mBlockController = new BlockController(act);
+    unblockEverything();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -234,7 +238,7 @@ public class RubikPreRender implements EffectController, TwistyPreRender
   private void finishRotationNow()
     {
     mFinishRotation = false;
-    blockEverything();
+    blockEverything(BlockController.RUBIK_PLACE_0);
     mRotationFinishedID = mNewObject.finishRotationNow(this, mNearestAngle);
 
     if( mRotationFinishedID==0 ) // failed to add effect - should never happen
@@ -251,7 +255,7 @@ public class RubikPreRender implements EffectController, TwistyPreRender
 
     if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
       {
-      blockEverything();
+      blockEverything(BlockController.RUBIK_PLACE_1);
       createObjectNow(mNextObject, mNextSize, null);
       doEffectNow( BaseEffect.Type.SIZECHANGE );
       }
@@ -265,7 +269,7 @@ public class RubikPreRender implements EffectController, TwistyPreRender
 
     if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
       {
-      blockEverything();
+      blockEverything(BlockController.RUBIK_PLACE_2);
       createObjectNow(mNextObject, mNextSize, mNextMoves);
       doEffectNow( BaseEffect.Type.SIZECHANGE );
       }
@@ -281,7 +285,7 @@ public class RubikPreRender implements EffectController, TwistyPreRender
     {
     mScrambleObject = false;
     mIsSolved       = false;
-    blockEverything();
+    blockEverything(BlockController.RUBIK_PLACE_3);
     RubikScores.getInstance().incrementNumPlays();
     doEffectNow( BaseEffect.Type.SCRAMBLE );
     }
@@ -291,7 +295,7 @@ public class RubikPreRender implements EffectController, TwistyPreRender
   private void solveObjectNow()
     {
     mSolveObject = false;
-    blockEverything();
+    blockEverything(BlockController.RUBIK_PLACE_4);
     doEffectNow( BaseEffect.Type.SOLVE );
     }
 
@@ -542,17 +546,20 @@ public class RubikPreRender implements EffectController, TwistyPreRender
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void blockEverything()
+  public void blockEverything(int place)
     {
     mUIBlocked   = true;
     mTouchBlocked= true;
+    mBlockController.touchBlocked(place);
+    mBlockController.uiBlocked(place);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void blockTouch()
+  public void blockTouch(int place)
     {
     mTouchBlocked= true;
+    mBlockController.touchBlocked(place);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -561,6 +568,8 @@ public class RubikPreRender implements EffectController, TwistyPreRender
     {
     mUIBlocked   = false;
     mTouchBlocked= false;
+    mBlockController.touchUnblocked();
+    mBlockController.uiUnblocked();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -568,6 +577,15 @@ public class RubikPreRender implements EffectController, TwistyPreRender
   public void unblockTouch()
     {
     mTouchBlocked= false;
+    mBlockController.touchUnblocked();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void unblockUI()
+    {
+    mUIBlocked= false;
+    mBlockController.uiUnblocked();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
diff --git a/src/main/java/org/distorted/tutorials/TutorialPreRender.java b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
index c91b3e4b..559aa36a 100644
--- a/src/main/java/org/distorted/tutorials/TutorialPreRender.java
+++ b/src/main/java/org/distorted/tutorials/TutorialPreRender.java
@@ -24,6 +24,7 @@ import android.content.res.Resources;
 
 import org.distorted.effects.BaseEffect;
 import org.distorted.effects.EffectController;
+import org.distorted.helpers.BlockController;
 import org.distorted.helpers.MovesFinished;
 import org.distorted.helpers.TwistyPreRender;
 import org.distorted.objects.ObjectList;
@@ -50,6 +51,7 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
   private long mAddRotationID, mRemoveRotationID;
   private int mNearestAngle;
   private int mScrambleObjectNum;
+  private final BlockController mBlockController;
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
@@ -66,8 +68,6 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
     mSolveObject    = false;
     mScrambleObject = false;
 
-    unblockEverything();
-
     mOldObject      = null;
     mNewObject      = null;
 
@@ -75,6 +75,10 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
     mScrambleObjectNum = 0;
 
     mRemovePatternRotation= false;
+
+    TutorialActivity act = (TutorialActivity)mView.getContext();
+    mBlockController = new BlockController(act);
+    unblockEverything();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -174,7 +178,7 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
   private void finishRotationNow()
     {
     mFinishRotation = false;
-    blockEverything();
+    blockEverything(BlockController.TUTORIAL_PLACE_0);
     mRotationFinishedID = mNewObject.finishRotationNow(this, mNearestAngle);
 
     if( mRotationFinishedID==0 ) // failed to add effect - should never happen
@@ -191,7 +195,7 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
 
     if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
       {
-      blockEverything();
+      blockEverything(BlockController.TUTORIAL_PLACE_1);
       createObjectNow(mNextObject, mNextSize);
       doEffectNow( BaseEffect.Type.SIZECHANGE );
       }
@@ -205,7 +209,7 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
 
     if ( mNewObject==null || mNewObject.getObjectList()!=mNextObject || mNewObject.getNumLayers()!=mNextSize)
       {
-      blockEverything();
+      blockEverything(BlockController.TUTORIAL_PLACE_2);
       createObjectNow(mNextObject, mNextSize);
       doEffectNow( BaseEffect.Type.SIZECHANGE );
       }
@@ -221,7 +225,7 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
     {
     mScrambleObject = false;
     mIsSolved       = false;
-    blockEverything();
+    blockEverything(BlockController.TUTORIAL_PLACE_3);
     doEffectNow( BaseEffect.Type.SCRAMBLE );
     }
 
@@ -230,7 +234,7 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
   private void solveObjectNow()
     {
     mSolveObject = false;
-    blockEverything();
+    blockEverything(BlockController.TUTORIAL_PLACE_4);
     doEffectNow( BaseEffect.Type.SOLVE );
     }
 
@@ -336,17 +340,20 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void blockEverything()
+  public void blockEverything(int place)
     {
     mUIBlocked   = true;
     mTouchBlocked= true;
+    mBlockController.touchBlocked(place);
+    mBlockController.uiBlocked(place);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
-  public void blockTouch()
+  public void blockTouch(int place)
     {
     mTouchBlocked= true;
+    mBlockController.touchBlocked(place);
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -355,6 +362,8 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
     {
     mUIBlocked   = false;
     mTouchBlocked= false;
+    mBlockController.touchUnblocked();
+    mBlockController.uiUnblocked();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
@@ -362,6 +371,15 @@ public class TutorialPreRender implements EffectController, TwistyPreRender
   public void unblockTouch()
     {
     mTouchBlocked= false;
+    mBlockController.touchUnblocked();
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void unblockUI()
+    {
+    mUIBlocked= false;
+    mBlockController.uiUnblocked();
     }
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
