commit c7238c6778371f0358695eba3a82f1843c6a7f3d
Author: Leszek Koltunski <leszek@koltunski.pl>
Date:   Mon Aug 15 14:17:59 2022 +0200

    IAP part 5: new 'Purchase' activity.

diff --git a/src/main/AndroidManifest.xml b/src/main/AndroidManifest.xml
index 975b4cdc..524bbe22 100644
--- a/src/main/AndroidManifest.xml
+++ b/src/main/AndroidManifest.xml
@@ -32,6 +32,7 @@
         <activity android:name="org.distorted.config.ConfigActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.bandaged.BandagedCreatorActivity" android:exported="false" android:screenOrientation="portrait"/>
         <activity android:name="org.distorted.bandaged.BandagedPlayActivity" android:exported="false" android:screenOrientation="portrait"/>
+        <activity android:name="org.distorted.purchase.PurchaseActivity" android:exported="false" android:screenOrientation="portrait"/>
 
         <service
             android:name="org.distorted.messaging.RubikMessagingService"
diff --git a/src/main/java/org/distorted/bandaged/BandagedCreatorActivity.java b/src/main/java/org/distorted/bandaged/BandagedCreatorActivity.java
index 65900a9c..d7050c8d 100644
--- a/src/main/java/org/distorted/bandaged/BandagedCreatorActivity.java
+++ b/src/main/java/org/distorted/bandaged/BandagedCreatorActivity.java
@@ -25,8 +25,6 @@ import android.widget.LinearLayout;
 
 import androidx.appcompat.app.AppCompatActivity;
 
-import com.google.firebase.analytics.FirebaseAnalytics;
-
 import org.distorted.dialogs.RubikDialogError;
 import org.distorted.external.RubikFiles;
 import org.distorted.library.main.DistortedLibrary;
@@ -54,7 +52,6 @@ public class BandagedCreatorActivity extends AppCompatActivity
                                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
 
-    private FirebaseAnalytics mFirebaseAnalytics;
     private static int mScreenWidth, mScreenHeight;
     private int mCurrentApiVersion;
     private BandagedCreatorScreen mScreen;
@@ -69,8 +66,6 @@ public class BandagedCreatorActivity extends AppCompatActivity
       setTheme(R.style.MaterialThemeNoActionBar);
       setContentView(R.layout.bandaged);
 
-      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
-
       DisplayMetrics displaymetrics = new DisplayMetrics();
       getWindowManager().getDefaultDisplay().getRealMetrics(displaymetrics);
       mScreenWidth =displaymetrics.widthPixels;
@@ -231,13 +226,6 @@ public class BandagedCreatorActivity extends AppCompatActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public FirebaseAnalytics getAnalytics()
-      {
-      return mFirebaseAnalytics;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void changeObject(int x, int y, int z)
diff --git a/src/main/java/org/distorted/bandaged/BandagedPlayActivity.java b/src/main/java/org/distorted/bandaged/BandagedPlayActivity.java
index fed0c894..6fc8b727 100644
--- a/src/main/java/org/distorted/bandaged/BandagedPlayActivity.java
+++ b/src/main/java/org/distorted/bandaged/BandagedPlayActivity.java
@@ -22,8 +22,6 @@ import android.widget.LinearLayout;
 
 import androidx.appcompat.app.AppCompatActivity;
 
-import com.google.firebase.analytics.FirebaseAnalytics;
-
 import org.distorted.dialogs.RubikDialogError;
 import org.distorted.external.RubikFiles;
 import org.distorted.library.main.DistortedLibrary;
@@ -50,7 +48,6 @@ public class BandagedPlayActivity extends AppCompatActivity
                                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
 
-    private FirebaseAnalytics mFirebaseAnalytics;
     private static int mScreenWidth, mScreenHeight;
     private int mCurrentApiVersion;
     private BandagedPlayScreen mScreen;
@@ -70,8 +67,6 @@ public class BandagedPlayActivity extends AppCompatActivity
       Bundle b = getIntent().getExtras();
       mObjectName = b!=null ? b.getString("name") : "";
 
-      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
-
       DisplayMetrics displaymetrics = new DisplayMetrics();
       getWindowManager().getDefaultDisplay().getRealMetrics(displaymetrics);
       mScreenWidth =displaymetrics.widthPixels;
@@ -267,13 +262,6 @@ public class BandagedPlayActivity extends AppCompatActivity
       changeIfDifferent(name,control);
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public FirebaseAnalytics getAnalytics()
-      {
-      return mFirebaseAnalytics;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public int getHeightBar()
diff --git a/src/main/java/org/distorted/config/ConfigActivity.java b/src/main/java/org/distorted/config/ConfigActivity.java
index bd76f54c..1e7928f7 100644
--- a/src/main/java/org/distorted/config/ConfigActivity.java
+++ b/src/main/java/org/distorted/config/ConfigActivity.java
@@ -19,7 +19,6 @@ import android.view.ViewGroup;
 import android.view.WindowManager;
 import android.widget.LinearLayout;
 import androidx.appcompat.app.AppCompatActivity;
-import com.google.firebase.analytics.FirebaseAnalytics;
 
 import org.distorted.library.main.DistortedLibrary;
 import org.distorted.objectlib.main.ObjectControl;
@@ -45,7 +44,6 @@ public class ConfigActivity extends AppCompatActivity
                                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
 
-    private FirebaseAnalytics mFirebaseAnalytics;
     private static int mScreenWidth, mScreenHeight;
     private int mCurrentApiVersion;
     private ConfigScreen mScreen;
@@ -66,8 +64,6 @@ public class ConfigActivity extends AppCompatActivity
 
       if(b != null) mObjectOrdinal = b.getInt("obj");
 
-      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
-
       DisplayMetrics displaymetrics = new DisplayMetrics();
       getWindowManager().getDefaultDisplay().getRealMetrics(displaymetrics);
       mScreenWidth =displaymetrics.widthPixels;
@@ -237,13 +233,6 @@ public class ConfigActivity extends AppCompatActivity
         }
       }
 
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public FirebaseAnalytics getAnalytics()
-      {
-      return mFirebaseAnalytics;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public int getHeightBar()
diff --git a/src/main/java/org/distorted/main/RubikActivity.java b/src/main/java/org/distorted/main/RubikActivity.java
index f13488d2..723f4868 100644
--- a/src/main/java/org/distorted/main/RubikActivity.java
+++ b/src/main/java/org/distorted/main/RubikActivity.java
@@ -48,6 +48,7 @@ import org.distorted.external.RubikScores;
 import org.distorted.external.RubikNetwork;
 import org.distorted.objects.RubikObject;
 import org.distorted.objects.RubikObjectList;
+import org.distorted.purchase.PurchaseActivity;
 import org.distorted.screens.RubikScreenSolving;
 import org.distorted.screens.ScreenList;
 import org.distorted.screens.RubikScreenPlay;
@@ -656,6 +657,15 @@ public class RubikActivity extends AppCompatActivity
       startActivity(intent);
       }
 
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void switchToPurchase(int objectOrdinal)
+      {
+      Intent intent = new Intent(this, PurchaseActivity.class);
+      intent.putExtra("obj", objectOrdinal);
+      startActivity(intent);
+      }
+
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public void reloadObject(String shortName)
diff --git a/src/main/java/org/distorted/purchase/PurchaseActivity.java b/src/main/java/org/distorted/purchase/PurchaseActivity.java
new file mode 100644
index 00000000..f9464e66
--- /dev/null
+++ b/src/main/java/org/distorted/purchase/PurchaseActivity.java
@@ -0,0 +1,308 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.purchase;
+
+import android.os.Build;
+import android.os.Bundle;
+import android.util.DisplayMetrics;
+import android.view.View;
+import android.view.ViewGroup;
+import android.view.WindowManager;
+import android.widget.LinearLayout;
+
+import androidx.appcompat.app.AppCompatActivity;
+
+import org.distorted.dialogs.RubikDialogError;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.main.R;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObject;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+
+import java.io.InputStream;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PurchaseActivity extends AppCompatActivity
+{
+    private static final int ACTIVITY_NUMBER = 5;
+    private static final float RATIO_BAR  = 0.10f;
+
+    public static final float DIALOG_BUTTON_SIZE  = 0.06f;
+    public static final float MENU_BIG_TEXT_SIZE  = 0.05f;
+
+    public static final int FLAGS =  View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION
+                                   | View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                                   | View.SYSTEM_UI_FLAG_HIDE_NAVIGATION
+                                   | View.SYSTEM_UI_FLAG_FULLSCREEN
+                                   | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
+
+    private static int mScreenWidth, mScreenHeight;
+    private int mCurrentApiVersion;
+    private PurchaseScreen mScreen;
+    private int mObjectOrdinal;
+    private int mHeightBar;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    protected void onCreate(Bundle savedState)
+      {
+      super.onCreate(savedState);
+      DistortedLibrary.onCreate(ACTIVITY_NUMBER);
+      setTheme(R.style.MaterialThemeNoActionBar);
+      setContentView(R.layout.purchase);
+
+      Bundle b = getIntent().getExtras();
+
+      if(b != null) mObjectOrdinal = b.getInt("obj");
+
+      DisplayMetrics displaymetrics = new DisplayMetrics();
+      getWindowManager().getDefaultDisplay().getRealMetrics(displaymetrics);
+      mScreenWidth =displaymetrics.widthPixels;
+      mScreenHeight=displaymetrics.heightPixels;
+
+      hideNavigationBar();
+      cutoutHack();
+      computeBarHeights();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// this does not include possible insets
+
+    private void computeBarHeights()
+      {
+      int barHeight = (int)(mScreenHeight*RATIO_BAR);
+      mHeightBar = barHeight;
+
+      LinearLayout layout = findViewById(R.id.lowerBar);
+      ViewGroup.LayoutParams params = layout.getLayoutParams();
+      params.height = barHeight;
+      layout.setLayoutParams(params);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void hideNavigationBar()
+      {
+      mCurrentApiVersion = Build.VERSION.SDK_INT;
+
+      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT)
+        {
+        final View decorView = getWindow().getDecorView();
+
+        decorView.setSystemUiVisibility(FLAGS);
+
+        decorView.setOnSystemUiVisibilityChangeListener(new View.OnSystemUiVisibilityChangeListener()
+          {
+          @Override
+          public void onSystemUiVisibilityChange(int visibility)
+            {
+            if((visibility & View.SYSTEM_UI_FLAG_FULLSCREEN) == 0)
+              {
+              decorView.setSystemUiVisibility(FLAGS);
+              }
+            }
+          });
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// do not avoid cutouts
+
+    private void cutoutHack()
+      {
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P)
+        {
+        getWindow().getAttributes().layoutInDisplayCutoutMode = WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_SHORT_EDGES;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onWindowFocusChanged(boolean hasFocus)
+      {
+      super.onWindowFocusChanged(hasFocus);
+
+      if(mCurrentApiVersion >= Build.VERSION_CODES.KITKAT && hasFocus)
+        {
+        getWindow().getDecorView().setSystemUiVisibility(FLAGS);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onPause() 
+      {
+      super.onPause();
+      PurchaseSurfaceView view = findViewById(R.id.purchaseSurfaceView);
+      view.onPause();
+      DistortedLibrary.onPause(ACTIVITY_NUMBER);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onResume() 
+      {
+      super.onResume();
+      DistortedLibrary.onResume(ACTIVITY_NUMBER);
+      PurchaseSurfaceView view = findViewById(R.id.purchaseSurfaceView);
+      view.onResume();
+
+      if( mScreen==null ) mScreen = new PurchaseScreen();
+      mScreen.onAttachedToWindow(this,mObjectOrdinal);
+
+      if( mObjectOrdinal>=0 && mObjectOrdinal< RubikObjectList.getNumObjects() )
+        {
+        RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+        changeIfDifferent(object,mObjectOrdinal,view.getObjectControl());
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+    
+    @Override
+    protected void onDestroy() 
+      {
+      super.onDestroy();
+      DistortedLibrary.onDestroy(ACTIVITY_NUMBER);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void OpenGLError()
+      {
+      RubikDialogError errDiag = new RubikDialogError();
+      errDiag.show(getSupportFragmentManager(), null);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    private void changeIfDifferent(RubikObject object,int ordinal,ObjectControl control)
+      {
+      if( object!=null )
+        {
+        int meshState          = object.getMeshState();
+        int iconMode           = TwistyObject.MODE_NORM;
+        InputStream jsonStream = object.getObjectStream(this);
+        InputStream meshStream = object.getMeshStream(this);
+        String name            = object.getUpperName();
+
+        control.changeIfDifferent(ordinal,name,meshState,iconMode,jsonStream,meshStream);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void changeObject(int ordinal)
+      {
+      mObjectOrdinal = ordinal;
+      RubikObject object = RubikObjectList.getObject(ordinal);
+      PurchaseSurfaceView view = findViewById(R.id.purchaseSurfaceView);
+      ObjectControl control = view.getObjectControl();
+      changeIfDifferent(object,ordinal,control);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public void changeMeshState(RubikObject object, int ordinal)
+      {
+      if( object!=null )
+        {
+        PurchaseSurfaceView view = findViewById(R.id.purchaseSurfaceView);
+        ObjectControl control = view.getObjectControl();
+
+        int meshState          = object.getMeshState();
+        int iconMode           = TwistyObject.MODE_NORM;
+        InputStream jsonStream = object.getObjectStream(this);
+        InputStream meshStream = object.getMeshStream(this);
+
+        control.changeObject(ordinal,meshState,iconMode,jsonStream,meshStream);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getHeightBar()
+      {
+      return mHeightBar;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getScreenWidthInPixels()
+      {
+      return mScreenWidth;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public int getScreenHeightInPixels()
+      {
+      return mScreenHeight;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public ObjectControl getControl()
+      {
+      PurchaseSurfaceView view = findViewById(R.id.purchaseSurfaceView);
+      return view.getObjectControl();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public static int getDrawableSize()
+      {
+      if( mScreenHeight<1000 )
+        {
+        return 0;
+        }
+      if( mScreenHeight<1600 )
+        {
+        return 1;
+        }
+      if( mScreenHeight<1900 )
+        {
+        return 2;
+        }
+
+      return 3;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public static int getDrawable(int small, int medium, int big, int huge)
+      {
+      int size = getDrawableSize();
+
+      switch(size)
+        {
+        case 0 : return small;
+        case 1 : return medium;
+        case 2 : return big;
+        default: return huge;
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public boolean isVertical()
+      {
+      PurchaseSurfaceView view = findViewById(R.id.purchaseSurfaceView);
+      return view.isVertical();
+      }
+}
diff --git a/src/main/java/org/distorted/purchase/PurchaseObjectLibInterface.java b/src/main/java/org/distorted/purchase/PurchaseObjectLibInterface.java
new file mode 100644
index 00000000..99b5c224
--- /dev/null
+++ b/src/main/java/org/distorted/purchase/PurchaseObjectLibInterface.java
@@ -0,0 +1,130 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2021 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.purchase;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.library.message.EffectMessageSender;
+import org.distorted.objectlib.BuildConfig;
+import org.distorted.objectlib.helpers.BlockController;
+import org.distorted.objectlib.helpers.ObjectLibInterface;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PurchaseObjectLibInterface implements ObjectLibInterface
+{
+  public void onWinEffectFinished(long startTime, long endTime, String debug, int scrambleNum) { }
+  public void onScrambleEffectFinished() { }
+  public void onBeginRotation() { }
+  public void onSolved() { }
+  public void onObjectCreated(long time) { }
+  public void onReplaceModeDown(int cubit, int face) { }
+  public void onReplaceModeUp() { }
+  public void onFinishRotation(int axis, int row, int angle) { }
+  public void failedToDrag() { }
+  public void reportJSONError(String error, int ordinal) { }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportProblem(String problem, boolean reportException)
+    {
+    if( BuildConfig.DEBUG )
+      {
+      android.util.Log.e("interface", problem);
+      }
+    else
+      {
+      if( reportException )
+        {
+        Exception ex = new Exception(problem);
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.setCustomKey("problem" , problem);
+        crashlytics.recordException(ex);
+        }
+      else
+        {
+        FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+        crashlytics.log(problem);
+        }
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportUIProblem(int place, long pause, long resume, long time)
+    {
+    String error = "UI BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportTouchProblem(int place, long pause, long resume, long time)
+    {
+    String error = "TOUCH BLOCK "+place+" blocked for "+time;
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause );
+      crashlytics.setCustomKey("resume", resume);
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void reportThreadProblem(int place, long pause, long resume, long time)
+    {
+    String error = EffectMessageSender.reportState();
+
+    if( BuildConfig.DEBUG )
+       {
+       android.util.Log.e("D", error);
+       }
+    else
+      {
+      Exception ex = new Exception(error);
+      FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+      crashlytics.setCustomKey("pause" , pause  );
+      crashlytics.setCustomKey("resume", resume );
+      crashlytics.recordException(ex);
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  public void reportBlockProblem(int type, int place, long pause, long resume, long time)
+    {
+    switch(type)
+      {
+      case BlockController.TYPE_UI    : reportUIProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_TOUCH : reportTouchProblem(place,pause,resume,time); break;
+      case BlockController.TYPE_THREAD: reportThreadProblem(place,pause,resume,time); break;
+      }
+    }
+}
diff --git a/src/main/java/org/distorted/purchase/PurchaseRenderer.java b/src/main/java/org/distorted/purchase/PurchaseRenderer.java
new file mode 100644
index 00000000..ab112798
--- /dev/null
+++ b/src/main/java/org/distorted/purchase/PurchaseRenderer.java
@@ -0,0 +1,88 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2019 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.purchase;
+
+import android.opengl.GLSurfaceView;
+
+import org.distorted.library.effect.EffectType;
+import org.distorted.library.effect.VertexEffectQuaternion;
+import org.distorted.library.effect.VertexEffectRotate;
+import org.distorted.library.main.DistortedLibrary;
+import org.distorted.library.main.DistortedScreen;
+import org.distorted.objectlib.effects.BaseEffect;
+import org.distorted.objectlib.main.ObjectControl;
+
+import javax.microedition.khronos.egl.EGLConfig;
+import javax.microedition.khronos.opengles.GL10;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PurchaseRenderer implements GLSurfaceView.Renderer, DistortedLibrary.ExceptionListener
+{
+   private final PurchaseSurfaceView mView;
+   private final DistortedScreen mScreen;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   PurchaseRenderer(PurchaseSurfaceView v)
+     {
+     final float BRIGHTNESS = 0.333f;
+
+     mView = v;
+     mScreen = new DistortedScreen();
+     mScreen.glClearColor(BRIGHTNESS, BRIGHTNESS, BRIGHTNESS, 1.0f);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onDrawFrame(GL10 glUnused)
+     {
+     long time = System.currentTimeMillis();
+     mView.getObjectControl().preRender();
+     mScreen.render(time);
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceChanged(GL10 glUnused, int width, int height)
+      {
+      mScreen.resize(width,height);
+      mView.setScreenSize(width,height);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   @Override
+   public void onSurfaceCreated(GL10 glUnused, EGLConfig config)
+      {
+      DistortedLibrary.setMax(EffectType.VERTEX, ObjectControl.MAX_QUATS+1);
+      VertexEffectRotate.enable();
+      VertexEffectQuaternion.enable();
+      BaseEffect.Type.enableEffects();
+
+      DistortedLibrary.onSurfaceCreated(mView.getContext(),this,1);
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   public void distortedException(Exception ex)
+     {
+     android.util.Log.e("PURCHASE", "unexpected exception: "+ex.getMessage() );
+     }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+   DistortedScreen getScreen()
+     {
+     return mScreen;
+     }
+}
diff --git a/src/main/java/org/distorted/purchase/PurchaseScreen.java b/src/main/java/org/distorted/purchase/PurchaseScreen.java
new file mode 100644
index 00000000..b160d102
--- /dev/null
+++ b/src/main/java/org/distorted/purchase/PurchaseScreen.java
@@ -0,0 +1,319 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.purchase;
+
+import android.os.Build;
+import android.util.TypedValue;
+import android.view.Gravity;
+import android.view.View;
+import android.widget.GridLayout;
+import android.widget.ImageButton;
+import android.widget.LinearLayout;
+import android.widget.PopupWindow;
+import android.widget.ScrollView;
+import android.widget.TextView;
+
+import org.distorted.helpers.PopupCreator;
+import org.distorted.helpers.TransparentImageButton;
+import org.distorted.main.R;
+import org.distorted.main.RubikActivity;
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+
+import static android.view.View.inflate;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PurchaseScreen
+{
+  private static final int NUM_COLUMNS = 5;
+  private static final int[] mLocation = new int[2];
+
+  private TransparentImageButton mBackButton, mObjectButton, mPrevButton, mNextButton;
+  private TextView mMovesText;
+  private PopupWindow mObjectPopup;
+  private PurchaseScreenPane mPane;
+  private int mObjectOrdinal;
+  private int mColCount, mRowCount, mMaxRowCount;
+  private int mObjectSize;
+  private int mBarHeight;
+  private float mButtonSize;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupObjectWindow(final PurchaseActivity act, final float width, final float height)
+    {
+    int numObjects= RubikObjectList.getNumObjects();
+    mRowCount = (numObjects + NUM_COLUMNS-1) / NUM_COLUMNS;
+    mColCount = NUM_COLUMNS;
+
+    int cubeSize  = (int)(width/9);
+    int margin    = (int)(width*RubikActivity.LARGE_MARGIN);
+    int padding   = (int)(width*RubikActivity.POPUP_PADDING);
+    mObjectSize   = (int)(cubeSize + 2*margin + 0.5f);
+    mMaxRowCount  = (int)((height-mBarHeight)/mObjectSize);
+
+    ScrollView view = (ScrollView)inflate( act, R.layout.popup_object_simple, null);
+    GridLayout objectGrid = view.findViewById(R.id.objectGrid);
+
+    PopupCreator.createObjectGrid(objectGrid,act,mRowCount,mColCount,numObjects,margin,cubeSize,padding);
+
+    for(int child=0; child<numObjects; child++)
+      {
+      final RubikObject obj = RubikObjectList.getObject(child);
+      View v = objectGrid.getChildAt(child);
+      ImageButton button = PopupCreator.getButton(obj,v);
+      final int ordinal = child;
+
+      button.setOnClickListener( new View.OnClickListener()
+        {
+        @Override
+        public void onClick(View v)
+          {
+          if( act.getControl().isUINotBlocked() && mObjectOrdinal!=ordinal )
+            {
+            mObjectOrdinal = ordinal;
+            act.changeObject(mObjectOrdinal);
+            mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
+            mPane.updatePane(act,mObjectOrdinal);
+            }
+
+          mObjectPopup.dismiss();
+          }
+        });
+      }
+
+    mObjectPopup = new PopupWindow(act);
+    mObjectPopup.setFocusable(true);
+    mObjectPopup.setContentView(view);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupBackButton(final PurchaseActivity act)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_smallback,R.drawable.ui_medium_smallback, R.drawable.ui_big_smallback, R.drawable.ui_huge_smallback);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
+    mBackButton = new TransparentImageButton(act, icon, TransparentImageButton.GRAVITY_MIDDLE, params);
+
+    mBackButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        ObjectControl control = act.getControl();
+        if( control!=null ) control.unblockEverything();
+        act.finish();
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupObjectButton(final PurchaseActivity act, final int width)
+    {
+    final int margin= (int)(width*RubikActivity.SMALL_MARGIN);
+    final int icon = RubikActivity.getDrawable(R.drawable.ui_small_cube_menu,R.drawable.ui_medium_cube_menu, R.drawable.ui_big_cube_menu, R.drawable.ui_huge_cube_menu);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT,LinearLayout.LayoutParams.MATCH_PARENT);
+    mObjectButton = new TransparentImageButton(act, icon, TransparentImageButton.GRAVITY_MIDDLE, params);
+
+    mObjectButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View view)
+        {
+        if( mObjectPopup==null )
+          {
+          float height= act.getScreenHeightInPixels();
+          setupObjectWindow(act,width,height);
+          }
+
+        if( act.getControl().isUINotBlocked())
+          {
+          int rowCount = Math.min(mMaxRowCount,mRowCount);
+          View popupView = mObjectPopup.getContentView();
+          popupView.setSystemUiVisibility(RubikActivity.FLAGS);
+          displayPopup(act,view,mObjectPopup,mObjectSize*mColCount,mObjectSize*rowCount+5*margin,margin,margin);
+          }
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// work around lame bugs in Android's version <= 10 pop-up and split-screen modes
+
+  private void displayPopup(PurchaseActivity act, View view, PopupWindow window, int w, int h, int xoff, int yoff)
+    {
+    View topLayout = act.findViewById(R.id.mainLayout);
+    boolean isFullScreen;
+
+    if( topLayout!=null )
+      {
+      topLayout.getLocationOnScreen(mLocation);
+      isFullScreen = (mLocation[1]==0);
+      }
+    else
+      {
+      isFullScreen = true;
+      }
+
+    try
+      {
+      // if on Android 11 or we are fullscreen
+      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R || isFullScreen )
+        {
+        window.showAsDropDown(view, xoff, yoff, Gravity.CENTER);
+        window.update(view, w, h);
+        }
+      else  // Android 10 or below in pop-up mode or split-screen mode
+        {
+        view.getLocationOnScreen(mLocation);
+        int width  = view.getWidth();
+        int height = view.getHeight();
+        int x = mLocation[0]+(width-w)/2;
+        int y = mLocation[1]+height+yoff;
+
+        window.showAsDropDown(view);
+        window.update(x,y,w,h);
+        }
+      }
+    catch( IllegalArgumentException iae )
+      {
+      // ignore, this means window is 'not attached to window manager' -
+      // which most probably is because we are already exiting the app.
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void prevObject(PurchaseActivity act, int numObjects)
+    {
+    mObjectOrdinal--;
+    if( mObjectOrdinal<0 ) mObjectOrdinal=numObjects-1;
+
+    act.changeObject(mObjectOrdinal);
+    mPane.updatePane(act,mObjectOrdinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void nextObject(PurchaseActivity act, int numObjects)
+    {
+    mObjectOrdinal++;
+    if( mObjectOrdinal>=numObjects ) mObjectOrdinal=0;
+
+    act.changeObject(mObjectOrdinal);
+    mPane.updatePane(act,mObjectOrdinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupPrevButton(final PurchaseActivity act)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_left,R.drawable.ui_medium_left, R.drawable.ui_big_left, R.drawable.ui_huge_left);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mPrevButton = new TransparentImageButton(act,icon,TransparentImageButton.GRAVITY_MIDDLE,params);
+
+    mPrevButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        int numObjects = RubikObjectList.getNumObjects();
+        prevObject(act,numObjects);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupNextButton(final PurchaseActivity act)
+    {
+    int icon = RubikActivity.getDrawable(R.drawable.ui_small_right,R.drawable.ui_medium_right, R.drawable.ui_big_right, R.drawable.ui_huge_right);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,1.0f);
+    mNextButton = new TransparentImageButton(act,icon,TransparentImageButton.GRAVITY_MIDDLE,params);
+
+    mNextButton.setOnClickListener( new View.OnClickListener()
+      {
+      @Override
+      public void onClick(View v)
+        {
+        int numObjects = RubikObjectList.getNumObjects();
+        nextObject(act,numObjects);
+        mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
+        }
+      });
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void setupTextView(final PurchaseActivity act, final float width, int numObjects)
+    {
+    int padding = (int)(width*RubikActivity.PADDING);
+    int margin  = (int)(width*RubikActivity.SMALL_MARGIN);
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(0,LinearLayout.LayoutParams.MATCH_PARENT,2.0f);
+    params.topMargin    = margin;
+    params.bottomMargin = margin;
+    params.leftMargin   = margin;
+    params.rightMargin  = margin;
+
+    mMovesText = new TextView(act);
+    mMovesText.setTextSize(20);
+    mMovesText.setLayoutParams(params);
+    mMovesText.setPadding(padding,0,padding,0);
+    mMovesText.setGravity(Gravity.CENTER);
+    mMovesText.setTextSize(TypedValue.COMPLEX_UNIT_PX, mButtonSize);
+    mMovesText.setText(act.getString(R.string.mo_placeholder,mObjectOrdinal+1,numObjects));
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void onAttachedToWindow(final PurchaseActivity act, final int objectOrdinal)
+    {
+    int numObjects = RubikObjectList.getNumObjects();
+    int width = act.getScreenWidthInPixels();
+    mBarHeight = act.getHeightBar();
+    mButtonSize = width*RubikActivity.BUTTON_TEXT_SIZE;
+    mObjectOrdinal = objectOrdinal;
+
+    LinearLayout.LayoutParams paramsL = new LinearLayout.LayoutParams(width/4, LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsM = new LinearLayout.LayoutParams(width/2, LinearLayout.LayoutParams.MATCH_PARENT);
+    LinearLayout.LayoutParams paramsR = new LinearLayout.LayoutParams(width/4, LinearLayout.LayoutParams.MATCH_PARENT);
+
+    LinearLayout layoutLeft = new LinearLayout(act);
+    layoutLeft.setLayoutParams(paramsL);
+    LinearLayout layoutMid  = new LinearLayout(act);
+    layoutMid.setLayoutParams(paramsM);
+    LinearLayout layoutRight= new LinearLayout(act);
+    layoutRight.setLayoutParams(paramsR);
+
+    setupObjectButton(act,width);
+    setupPrevButton(act);
+    setupNextButton(act);
+    setupTextView(act,width,numObjects);
+    setupBackButton(act);
+
+    layoutLeft.addView(mObjectButton);
+    layoutMid.addView(mPrevButton);
+    layoutMid.addView(mMovesText);
+    layoutMid.addView(mNextButton);
+    layoutRight.addView(mBackButton);
+
+    LinearLayout layout = act.findViewById(R.id.lowerBar);
+    layout.removeAllViews();
+    layout.addView(layoutLeft);
+    layout.addView(layoutMid);
+    layout.addView(layoutRight);
+
+    mPane = new PurchaseScreenPane(act,mObjectOrdinal);
+    }
+}
diff --git a/src/main/java/org/distorted/purchase/PurchaseScreenPane.java b/src/main/java/org/distorted/purchase/PurchaseScreenPane.java
new file mode 100644
index 00000000..6ae2c9d2
--- /dev/null
+++ b/src/main/java/org/distorted/purchase/PurchaseScreenPane.java
@@ -0,0 +1,197 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.purchase;
+
+import android.graphics.PorterDuff;
+import android.util.TypedValue;
+import android.widget.ImageView;
+import android.widget.LinearLayout;
+import android.widget.RadioButton;
+import android.widget.RadioGroup;
+import android.widget.TextView;
+
+import org.distorted.main.R;
+import org.distorted.objectlib.json.JsonReader;
+import org.distorted.objects.RubikObject;
+import org.distorted.objects.RubikObjectList;
+
+import java.io.InputStream;
+
+import static org.distorted.objectlib.main.TwistyObject.MESH_FAST;
+import static org.distorted.objectlib.main.TwistyObject.MESH_NICE;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PurchaseScreenPane
+{
+  private static final int[] IDS =
+    {
+    R.id.purchaseDifficulty0,
+    R.id.purchaseDifficulty1,
+    R.id.purchaseDifficulty2,
+    R.id.purchaseDifficulty3,
+    R.id.purchaseDifficulty4
+    };
+
+  private static final int[] IMAGES =
+    {
+    R.drawable.difficulty1,
+    R.drawable.difficulty2,
+    R.drawable.difficulty3,
+    R.drawable.difficulty4,
+    R.drawable.difficulty5,
+    };
+
+  private static final int NUM_IDS         = IDS.length;
+  public  static final float PADDING_RATIO = 0.025f;
+  private static final float TEXT_RATIO    = 0.042f;
+  private static final float RADIO_RATIO   = 0.900f;
+
+  private int mObjectOrdinal;
+  private boolean mProgramatic;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  private void switchMeshState(PurchaseActivity act, int meshState)
+    {
+    RubikObjectList.setMeshState(mObjectOrdinal,meshState);
+    RubikObject object = RubikObjectList.getObject(mObjectOrdinal);
+    act.changeMeshState(object,mObjectOrdinal);
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  void updatePane(PurchaseActivity act, int objectOrdinal)
+    {
+    RubikObject object = RubikObjectList.getObject(objectOrdinal);
+
+    if( object!=null )
+      {
+      InputStream stream = object.getObjectStream(act);
+
+      mObjectOrdinal = objectOrdinal;
+      JsonReader reader = JsonReader.getInstance();
+      String name,author;
+      int year, difficulty;
+
+      try
+        {
+        reader.parseJsonFileMetadata(stream);
+        name       = reader.getObjectName();
+        author     = reader.getInventor();
+        year       = reader.getYearOfInvention();
+        difficulty = reader.getComplexity();
+        }
+      catch(Exception ex)
+        {
+        name = "?";
+        author = "?";
+        year = 0;
+        difficulty = 0;
+        }
+
+      String both = year>0 ? author+" "+year : author;
+
+      LinearLayout layout = act.findViewById(R.id.purchaseLayout);
+      TextView view = layout.findViewById(R.id.purchaseDetailsName2);
+      view.setText(name);
+      view = layout.findViewById(R.id.purchaseDetailsAuthor2);
+      view.setText(both);
+
+      if( difficulty<0       ) difficulty=0;
+      if( difficulty>NUM_IDS ) difficulty=NUM_IDS;
+
+      for(int i=0; i<NUM_IDS; i++)
+        {
+        ImageView image = layout.findViewById(IDS[i]);
+        image.setImageResource(IMAGES[i]);
+        image.setColorFilter( difficulty==i ? 0xffff0000 : 0xffffffff, PorterDuff.Mode.MULTIPLY );
+        }
+
+      int meshState = object.getMeshState();
+      int id = meshState==MESH_NICE ? R.id.meshNice : R.id.meshSimple;
+      RadioButton button = act.findViewById(id);
+
+      mProgramatic = true;
+      button.setChecked(true);
+      mProgramatic = false;
+      }
+    }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+  PurchaseScreenPane(final PurchaseActivity act, int objectOrdinal)
+    {
+    int width = act.getScreenWidthInPixels();
+    float textSize = width*TEXT_RATIO;
+    int padding = (int)(width*PADDING_RATIO);
+
+    LinearLayout configLayout    = act.findViewById(R.id.purchaseLayout);
+    LinearLayout nameLayout      = configLayout.findViewById(R.id.purchaseLayoutName);
+    LinearLayout authorLayout    = configLayout.findViewById(R.id.purchaseLayoutAuthor);
+    LinearLayout difficultyLayout= configLayout.findViewById(R.id.purchaseLayoutDifficulty);
+    LinearLayout meshLayout      = configLayout.findViewById(R.id.purchaseLayoutMesh);
+
+    nameLayout.setPadding(padding,padding,padding,padding/2);
+    authorLayout.setPadding(padding,padding/2,padding,padding/2);
+    difficultyLayout.setPadding(padding,padding/2,padding,padding/2);
+    meshLayout.setPadding(padding,padding/2,padding,padding);
+
+    LinearLayout.LayoutParams params = new LinearLayout.LayoutParams(LinearLayout.LayoutParams.MATCH_PARENT, 0, 1.00f);
+    params.bottomMargin = 0;
+    params.topMargin    = padding;
+    params.leftMargin   = padding;
+    params.rightMargin  = padding;
+
+    nameLayout.setLayoutParams(params);
+    authorLayout.setLayoutParams(params);
+    difficultyLayout.setLayoutParams(params);
+    meshLayout.setLayoutParams(params);
+
+    TextView text;
+    text = nameLayout.findViewById(R.id.purchaseDetailsName1);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+    text = nameLayout.findViewById(R.id.purchaseDetailsName2);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+    text = authorLayout.findViewById(R.id.purchaseDetailsAuthor1);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+    text = authorLayout.findViewById(R.id.purchaseDetailsAuthor2);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+    text = difficultyLayout.findViewById(R.id.purchaseDifficultyTitle);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+    text = meshLayout.findViewById(R.id.purchaseMeshTitle);
+    text.setTextSize(TypedValue.COMPLEX_UNIT_PX, textSize);
+
+    RadioButton butt1 = meshLayout.findViewById(R.id.meshNice);
+    butt1.setTextSize(TypedValue.COMPLEX_UNIT_PX, RADIO_RATIO*textSize);
+    RadioButton butt2 = meshLayout.findViewById(R.id.meshSimple);
+    butt2.setTextSize(TypedValue.COMPLEX_UNIT_PX, RADIO_RATIO*textSize);
+
+    LinearLayout layoutDiff = difficultyLayout.findViewById(R.id.purchaseDifficultyLayout);
+    layoutDiff.setPadding(padding,padding,padding,padding);
+
+    RadioGroup radioGroup = meshLayout.findViewById(R.id.meshRadioGroup);
+
+    radioGroup.setOnCheckedChangeListener(new RadioGroup.OnCheckedChangeListener()
+      {
+      @Override
+      public void onCheckedChanged(RadioGroup group, int checkedId)
+        {
+        if( !mProgramatic )
+          {
+          int meshState = checkedId == R.id.meshNice ? MESH_NICE : MESH_FAST;
+          switchMeshState( act, meshState );
+          }
+        }
+      });
+
+    updatePane(act,objectOrdinal);
+    }
+}
diff --git a/src/main/java/org/distorted/purchase/PurchaseSurfaceView.java b/src/main/java/org/distorted/purchase/PurchaseSurfaceView.java
new file mode 100644
index 00000000..3ac14437
--- /dev/null
+++ b/src/main/java/org/distorted/purchase/PurchaseSurfaceView.java
@@ -0,0 +1,129 @@
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// Copyright 2020 Leszek Koltunski                                                               //
+//                                                                                               //
+// This file is part of Magic Cube.                                                              //
+//                                                                                               //
+// Magic Cube is proprietary software licensed under an EULA which you should have received      //
+// along with the code. If not, check https://distorted.org/magic/License-Magic-Cube.html        //
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+package org.distorted.purchase;
+
+import android.app.ActivityManager;
+import android.content.Context;
+import android.content.pm.ConfigurationInfo;
+import android.opengl.GLES30;
+import android.opengl.GLSurfaceView;
+import android.util.AttributeSet;
+
+import com.google.firebase.crashlytics.FirebaseCrashlytics;
+
+import org.distorted.objectlib.main.ObjectControl;
+import org.distorted.objectlib.main.TwistyObjectNode;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+public class PurchaseSurfaceView extends GLSurfaceView
+{
+    private ObjectControl mObjectController;
+    private PurchaseRenderer mRenderer;
+    private int mScreenWidth, mScreenHeight;
+    private boolean mCreated;
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    void setScreenSize(int width, int height)
+      {
+      mScreenWidth = width;
+      mScreenHeight= height;
+      mObjectController.setScreenSize(width,height);
+      mObjectController.setObjectScale(1.00f);
+
+      if( !mCreated )
+        {
+        mCreated = true;
+        mObjectController.createNode(width,height);
+        TwistyObjectNode objectNode = mObjectController.getNode();
+        mRenderer.getScreen().attach(objectNode);
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    boolean isVertical()
+      {
+      return mScreenHeight>mScreenWidth;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    ObjectControl getObjectControl()
+      {
+      return mObjectController;
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+// PUBLIC API
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    public PurchaseSurfaceView(Context context, AttributeSet attrs)
+      {
+      super(context,attrs);
+
+      mCreated = false;
+
+      if(!isInEditMode())
+        {
+        PurchaseActivity act = (PurchaseActivity)context;
+        PurchaseObjectLibInterface ref = new PurchaseObjectLibInterface();
+        mObjectController = new ObjectControl(act,ref);
+        mObjectController.setRotateOnCreation(true);
+        mRenderer = new PurchaseRenderer(this);
+
+        final ActivityManager activityManager= (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
+
+        try
+          {
+          final ConfigurationInfo configurationInfo = activityManager.getDeviceConfigurationInfo();
+          int esVersion = configurationInfo.reqGlEsVersion>>16;
+          setEGLContextClientVersion(esVersion);
+          setRenderer(mRenderer);
+          }
+        catch(Exception ex)
+          {
+          act.OpenGLError();
+
+          String shading = GLES30.glGetString(GLES30.GL_SHADING_LANGUAGE_VERSION);
+          String version = GLES30.glGetString(GLES30.GL_VERSION);
+          String vendor  = GLES30.glGetString(GLES30.GL_VENDOR);
+          String renderer= GLES30.glGetString(GLES30.GL_RENDERER);
+
+          FirebaseCrashlytics crashlytics = FirebaseCrashlytics.getInstance();
+          crashlytics.setCustomKey("GLSL Version"  , shading );
+          crashlytics.setCustomKey("GL version"    , version );
+          crashlytics.setCustomKey("GL Vendor "    , vendor  );
+          crashlytics.setCustomKey("GLSL renderer" , renderer);
+          crashlytics.recordException(ex);
+          }
+        }
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onPause()
+      {
+      super.onPause();
+      mObjectController.onPause();
+      }
+
+///////////////////////////////////////////////////////////////////////////////////////////////////
+
+    @Override
+    public void onResume()
+      {
+      super.onResume();
+      mObjectController.onResume();
+      }
+}
+
diff --git a/src/main/java/org/distorted/screens/RubikScreenPlay.java b/src/main/java/org/distorted/screens/RubikScreenPlay.java
index 7701f05c..cd0ec504 100644
--- a/src/main/java/org/distorted/screens/RubikScreenPlay.java
+++ b/src/main/java/org/distorted/screens/RubikScreenPlay.java
@@ -201,12 +201,19 @@ public class RubikScreenPlay extends RubikScreenBase implements RubikNetwork.Upd
         @Override
         public void onClick(View v)
           {
-          if( act.getControl().isUINotBlocked() && ScreenList.getCurrentScreen()== ScreenList.PLAY )
+          if( obj!=null && act.getControl().isUINotBlocked() && ScreenList.getCurrentScreen()==ScreenList.PLAY )
             {
-            RubikObjectList.setCurrObject(ordinal);
-            act.changeObject(ordinal,true);
-            if( mMenuPopup!=null ) setupLevelColors(act);
-            mMovesController.clearMoves(act);
+            if( obj.isFree() )
+              {
+              RubikObjectList.setCurrObject(ordinal);
+              act.changeObject(ordinal,true);
+              if( mMenuPopup!=null ) setupLevelColors(act);
+              mMovesController.clearMoves(act);
+              }
+            else
+              {
+              act.switchToPurchase(ordinal);
+              }
             }
 
           if( mObjectPopup!=null ) mObjectPopup.dismiss();
diff --git a/src/main/java/org/distorted/tutorials/TutorialActivity.java b/src/main/java/org/distorted/tutorials/TutorialActivity.java
index d35ca5bf..b0852ccf 100644
--- a/src/main/java/org/distorted/tutorials/TutorialActivity.java
+++ b/src/main/java/org/distorted/tutorials/TutorialActivity.java
@@ -22,8 +22,6 @@ import android.widget.LinearLayout;
 
 import androidx.appcompat.app.AppCompatActivity;
 
-import com.google.firebase.analytics.FirebaseAnalytics;
-
 import org.distorted.library.main.DistortedLibrary;
 
 import org.distorted.objectlib.main.ObjectControl;
@@ -53,7 +51,6 @@ public class TutorialActivity extends AppCompatActivity
                                    | View.SYSTEM_UI_FLAG_FULLSCREEN
                                    | View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
 
-    private FirebaseAnalytics mFirebaseAnalytics;
     private static int mScreenWidth, mScreenHeight;
     private int mCurrentApiVersion;
     private TutorialScreen mScreen;
@@ -79,8 +76,6 @@ public class TutorialActivity extends AppCompatActivity
         mObjectOrdinal = b.getInt("obj");
         }
 
-      mFirebaseAnalytics = FirebaseAnalytics.getInstance(this);
-
       DisplayMetrics displaymetrics = new DisplayMetrics();
       getWindowManager().getDefaultDisplay().getRealMetrics(displaymetrics);
       mScreenWidth =displaymetrics.widthPixels;
@@ -238,13 +233,6 @@ public class TutorialActivity extends AppCompatActivity
 
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 // PUBLIC API
-///////////////////////////////////////////////////////////////////////////////////////////////////
-
-    public FirebaseAnalytics getAnalytics()
-      {
-      return mFirebaseAnalytics;
-      }
-
 ///////////////////////////////////////////////////////////////////////////////////////////////////
 
     public int getScreenWidthInPixels()
diff --git a/src/main/res/layout/purchase.xml b/src/main/res/layout/purchase.xml
new file mode 100644
index 00000000..9ba28e0f
--- /dev/null
+++ b/src/main/res/layout/purchase.xml
@@ -0,0 +1,220 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout  xmlns:android="http://schemas.android.com/apk/res/android"
+    android:id="@+id/mainLayout"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <org.distorted.purchase.PurchaseSurfaceView
+        android:id="@+id/purchaseSurfaceView"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1.3"/>
+
+    <LinearLayout
+        android:id="@+id/purchaseLayout"
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1.0"
+        android:background="@color/light_grey"
+        android:orientation="vertical" >
+
+        <LinearLayout
+            android:id="@+id/purchaseLayoutName"
+            android:layout_width="fill_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:background="@color/grey"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/purchaseDetailsName1"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1.0"
+                android:gravity="center_vertical|start"
+                android:paddingStart="5dp"
+                android:textSize="26sp"
+                android:singleLine="true"
+                android:maxLines="1"
+                android:text="@string/config_name"/>
+
+            <TextView
+                android:id="@+id/purchaseDetailsName2"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="2.1"
+                android:gravity="center_vertical|start"
+                android:paddingStart="5dp"
+                android:textSize="26sp"
+                android:singleLine="true"
+                android:maxLines="1"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/purchaseLayoutAuthor"
+            android:layout_width="fill_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:background="@color/grey"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/purchaseDetailsAuthor1"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1.0"
+                android:gravity="center_vertical|start"
+                android:paddingStart="5dp"
+                android:textSize="26sp"
+                android:singleLine="true"
+                android:maxLines="1"
+                android:text="@string/config_author"/>
+
+            <TextView
+                android:id="@+id/purchaseDetailsAuthor2"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="2.1"
+                android:gravity="center_vertical|start"
+                android:paddingStart="5dp"
+                android:textSize="26sp"
+                android:singleLine="true"
+                android:maxLines="1"/>
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:id="@+id/purchaseLayoutDifficulty"
+            android:layout_width="fill_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:background="@color/grey"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/purchaseDifficultyTitle"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1.0"
+                android:gravity="center_vertical|start"
+                android:paddingStart="5dp"
+                android:textSize="26sp"
+                android:singleLine="true"
+                android:maxLines="1"
+                android:text="@string/config_difficulty"/>
+
+            <LinearLayout
+                android:id="@+id/purchaseDifficultyLayout"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="2.5"
+                android:gravity="start"
+                android:paddingTop="20dp"
+                android:paddingBottom="20dp"
+                android:orientation="horizontal">
+
+                <ImageView
+                    android:id="@+id/purchaseDifficulty0"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"/>
+
+                <ImageView
+                    android:id="@+id/purchaseDifficulty1"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"/>
+
+                <ImageView
+                    android:id="@+id/purchaseDifficulty2"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"/>
+
+                <ImageView
+                    android:id="@+id/purchaseDifficulty3"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"/>
+
+                <ImageView
+                    android:id="@+id/purchaseDifficulty4"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"/>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+         <LinearLayout
+            android:id="@+id/purchaseLayoutMesh"
+            android:layout_width="fill_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1"
+            android:paddingLeft="5dp"
+            android:paddingRight="5dp"
+            android:background="@color/grey"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/purchaseMeshTitle"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="1.0"
+                android:gravity="center_vertical|start"
+                android:paddingStart="5dp"
+                android:textSize="26sp"
+                android:singleLine="true"
+                android:maxLines="1"
+                android:text="@string/config_mesh"/>
+
+            <RadioGroup
+                android:id="@+id/meshRadioGroup"
+                android:layout_width="0dp"
+                android:layout_height="match_parent"
+                android:layout_weight="2.1"
+                android:orientation="horizontal"
+                android:checkedButton="@+id/meshNice"
+                android:background="@color/grey">
+
+                <RadioButton
+                    android:id="@+id/meshSimple"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:buttonTint="@color/white"
+                    android:text="@string/config_mesh_fast"/>
+
+                <RadioButton
+                    android:id="@+id/meshNice"
+                    android:layout_width="0dp"
+                    android:layout_height="match_parent"
+                    android:layout_weight="1"
+                    android:buttonTint="@color/white"
+                    android:text="@string/config_mesh_nice"/>
+
+            </RadioGroup>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+    <LinearLayout
+        android:id="@+id/lowerBar"
+        android:layout_width="match_parent"
+        android:layout_height="100dp"
+        android:layout_gravity="end"
+        android:orientation="horizontal"
+        android:background="@color/light_grey">
+    </LinearLayout>
+
+</LinearLayout>
