Aperture: Implement leveler

Change-Id: If3a2c91d4b8ae43798314fd10486ad39690f16b9
diff --git a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
index 7776da2..b10238d 100644
--- a/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
+++ b/app/src/main/java/org/lineageos/aperture/CameraActivity.kt
@@ -70,6 +70,7 @@
 import com.google.android.material.slider.Slider
 import org.lineageos.aperture.ui.CountDownView
 import org.lineageos.aperture.ui.GridView
+import org.lineageos.aperture.ui.LevelerView
 import org.lineageos.aperture.utils.CameraFacing
 import org.lineageos.aperture.utils.CameraMode
 import org.lineageos.aperture.utils.CameraSoundsUtils
@@ -95,6 +96,7 @@
     private val galleryButton by lazy { findViewById<ImageView>(R.id.galleryButton) }
     private val gridButton by lazy { findViewById<ImageButton>(R.id.gridButton) }
     private val gridView by lazy { findViewById<GridView>(R.id.gridView) }
+    private val levelerView by lazy { findViewById<LevelerView>(R.id.levelerView) }
     private val micButton by lazy { findViewById<ImageButton>(R.id.micButton) }
     private val photoModeButton by lazy { findViewById<MaterialButton>(R.id.photoModeButton) }
     private val primaryBarLayout by lazy { findViewById<ConstraintLayout>(R.id.primaryBarLayout) }
@@ -463,6 +465,9 @@
         // Set bright screen
         setBrightScreen(sharedPreferences.brightScreen)
 
+        // Set leveler
+        setLeveler(sharedPreferences.leveler)
+
         // Reset tookSomething state
         tookSomething = false
 
@@ -1186,6 +1191,10 @@
         }
     }
 
+    private fun setLeveler(enabled: Boolean) {
+        levelerView.isVisible = enabled
+    }
+
     private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all {
         ContextCompat.checkSelfPermission(baseContext, it) == PackageManager.PERMISSION_GRANTED
     }
diff --git a/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt b/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt
index 14de37b..78c4efa 100644
--- a/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt
+++ b/app/src/main/java/org/lineageos/aperture/SharedPreferencesExt.kt
@@ -272,6 +272,16 @@
         putBoolean(SHUTTER_SOUND_KEY, value)
     }
 
+// Leveler
+private const val LEVELER_KEY = "leveler"
+private const val LEVELER_DEFAULT = false
+
+internal var SharedPreferences.leveler: Boolean
+    get() = getBoolean(LEVELER_KEY, LEVELER_DEFAULT)
+    set(value) = edit {
+        putBoolean(LEVELER_KEY, value)
+    }
+
 // Last saved URI
 private const val LAST_SAVED_URI_KEY = "saved_uri"
 
diff --git a/app/src/main/java/org/lineageos/aperture/ui/LevelerView.kt b/app/src/main/java/org/lineageos/aperture/ui/LevelerView.kt
new file mode 100644
index 0000000..8e13666
--- /dev/null
+++ b/app/src/main/java/org/lineageos/aperture/ui/LevelerView.kt
@@ -0,0 +1,139 @@
+/*
+ * Copyright (C) 2022 The LineageOS Project
+ *
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+package org.lineageos.aperture.ui
+
+import android.content.Context
+import android.graphics.Canvas
+import android.graphics.Paint
+import android.hardware.SensorManager
+import android.util.AttributeSet
+import android.view.OrientationEventListener
+import android.view.View
+import org.lineageos.aperture.R
+import kotlin.math.PI
+import kotlin.math.cos
+import kotlin.math.min
+import kotlin.math.roundToInt
+import kotlin.math.sin
+
+class LevelerView(context: Context, attributeSet: AttributeSet?) : View(context, attributeSet) {
+    private var currentOrientation = 0
+    private val orientationEventListener =
+        object : OrientationEventListener(context, SensorManager.SENSOR_DELAY_UI) {
+            override fun onOrientationChanged(orientation: Int) {
+                if (orientation == ORIENTATION_UNKNOWN) {
+                    return
+                }
+
+                currentOrientation = orientation
+                postInvalidate()
+            }
+        }
+
+    private val defaultLevelPaint = Paint().apply {
+        isAntiAlias = true
+        strokeWidth = 4f
+        style = Paint.Style.STROKE
+        color = 0x7FFFFFFF
+    }
+
+    private val defaultBasePaint = Paint().apply {
+        isAntiAlias = true
+        strokeWidth = 4f
+        style = Paint.Style.STROKE
+        color = 0x7FFFFFFF
+    }
+
+    private val highlightPaint = Paint().apply {
+        isAntiAlias = true
+        strokeWidth = 4f
+        style = Paint.Style.STROKE
+        color = context.getColor(R.color.yellow)
+    }
+
+    override fun setVisibility(visibility: Int) {
+        super.setVisibility(visibility)
+
+        if (visibility == VISIBLE) {
+            orientationEventListener.enable()
+        } else {
+            orientationEventListener.disable()
+        }
+    }
+
+    override fun onDraw(canvas: Canvas) {
+        super.onDraw(canvas)
+
+        val isLevel = isLevel()
+        drawBase(canvas, isLevel, isLandscape())
+
+        if (!isLevel) {
+            val radians = -((currentOrientation.toFloat() / 180F) * PI).toFloat()
+            drawLevelLine(canvas, radians)
+        }
+    }
+
+    private fun drawBase(canvas: Canvas, isLevel: Boolean, isLandscape: Boolean) {
+        val xLen = if (isLandscape) RELATIVE_BASE_LENGTH_Y else RELATIVE_BASE_LENGTH_X
+        val yLen = if (isLandscape) RELATIVE_BASE_LENGTH_X else RELATIVE_BASE_LENGTH_Y
+
+        val xLength = (min(width, height) * xLen)
+        val yLength = (min(width, height) * yLen)
+
+        val wCenter = width / 2
+        val hCenter = height / 2
+
+        val paint = if (isLevel) highlightPaint else defaultBasePaint
+
+        canvas.drawLine(
+            (wCenter - xLength).toFloat(),
+            hCenter.toFloat(),
+            (wCenter + xLength).toFloat(),
+            hCenter.toFloat(),
+            paint
+        )
+        canvas.drawLine(
+            wCenter.toFloat(),
+            (hCenter - yLength).toFloat(),
+            wCenter.toFloat(),
+            (hCenter + yLength).toFloat(),
+            paint
+        )
+    }
+
+    private fun drawLevelLine(canvas: Canvas, radians: Float) {
+        val length = (min(width, height) * RELATIVE_LINE_LENGTH).roundToInt()
+
+        val wCenter = width / 2
+        val hCenter = height / 2
+        val wOffset = cos(radians) * length
+        val hOffset = sin(radians) * length
+
+        val wStart = wCenter - wOffset
+        val hStart = hCenter - hOffset
+        val wEnd = wCenter + wOffset
+        val hEnd = hCenter + hOffset
+
+        canvas.drawLine(wStart, hStart, wEnd, hEnd, defaultLevelPaint)
+    }
+
+    private fun isLevel(): Boolean {
+        val o = currentOrientation % 90
+        return o < LEVEL_ZONE || (90 - o) < LEVEL_ZONE
+    }
+
+    private fun isLandscape(): Boolean {
+        return currentOrientation in 45..134 || currentOrientation in 225..314
+    }
+
+    companion object {
+        private const val LEVEL_ZONE = 2
+        private const val RELATIVE_LINE_LENGTH = 0.15
+        private const val RELATIVE_BASE_LENGTH_X = 0.15
+        private const val RELATIVE_BASE_LENGTH_Y = 0.01
+    }
+}
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_camera.xml b/app/src/main/res/layout/activity_camera.xml
index 3a8b935..7152ec7 100644
--- a/app/src/main/res/layout/activity_camera.xml
+++ b/app/src/main/res/layout/activity_camera.xml
@@ -180,6 +180,15 @@
         app:layout_constraintStart_toStartOf="@+id/viewFinder"
         app:layout_constraintTop_toTopOf="@+id/viewFinder" />
 
+    <org.lineageos.aperture.ui.LevelerView
+        android:id="@+id/levelerView"
+        android:layout_width="0dp"
+        android:layout_height="0dp"
+        app:layout_constraintBottom_toBottomOf="@+id/viewFinder"
+        app:layout_constraintEnd_toEndOf="@+id/viewFinder"
+        app:layout_constraintStart_toStartOf="@+id/viewFinder"
+        app:layout_constraintTop_toTopOf="@+id/viewFinder" />
+
     <org.lineageos.aperture.ui.CountDownView
         android:id="@+id/countDownView"
         android:layout_width="match_parent"
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
index d92087d..dfddf08 100644
--- a/app/src/main/res/values/colors.xml
+++ b/app/src/main/res/values/colors.xml
@@ -11,5 +11,7 @@
     <color name="gray_10">#FFF6FAFA</color>
     <color name="gray_60">#FF2A3232</color>
     <color name="dark_grey">#FF444444</color>
+    <color name="yellow">#E9B650</color>
+    <color name="blue">#4D84E9</color>
     <color name="rec_red">#FFE95950</color>
 </resources>
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index 63d87b7..7102c7a 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -47,6 +47,7 @@
     <!-- Preference Titles -->
     <string name="general_header">General</string>
     <string name="photos_header">Photos</string>
+    <string name="advanced_header">Advanced</string>
 
     <!-- General Preferences -->
     <string name="bright_screen_title">Bright screen</string>
@@ -63,6 +64,10 @@
     <string name="photo_capture_mode_minimize_latency">Minimize latency</string>
     <string name="photo_capture_mode_zsl">Zero shutter lag (experimental)</string>
 
+    <!-- Advanced Preferences -->
+    <string name="leveler_title">Leveler</string>
+    <string name="leveler_summary">Indicator that shows device orientation</string>
+
     <!-- Shortcuts -->
     <string name="shortcut_selfie">Take a selfie</string>
     <string name="shortcut_video">Take a video</string>
diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml
index d30f49b..58ed4bd 100644
--- a/app/src/main/res/xml/root_preferences.xml
+++ b/app/src/main/res/xml/root_preferences.xml
@@ -42,4 +42,16 @@
 
     </PreferenceCategory>
 
+    <PreferenceCategory
+        app:iconSpaceReserved="false"
+        app:title="@string/advanced_header">
+
+        <SwitchPreference
+            app:defaultValue="false"
+            app:iconSpaceReserved="false"
+            app:key="leveler"
+            app:summary="@string/leveler_summary"
+            app:title="@string/leveler_title" />
+    </PreferenceCategory>
+
 </PreferenceScreen>