diff --git a/.idea/misc.xml b/.idea/misc.xml index 195b12d..52a5b8f 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 6edfe79..8e8ff11 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -21,8 +21,8 @@ android { applicationId = "uk.akane.omni" minSdk = 26 targetSdk = 34 - versionCode = 2 - versionName = "1.1" + versionCode = 3 + versionName = "1.2" testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" } diff --git a/app/src/main/java/uk/akane/omni/logic/Extensions.kt b/app/src/main/java/uk/akane/omni/logic/Extensions.kt index ba2035e..69fb2dd 100644 --- a/app/src/main/java/uk/akane/omni/logic/Extensions.kt +++ b/app/src/main/java/uk/akane/omni/logic/Extensions.kt @@ -7,8 +7,6 @@ import android.content.res.Configuration import android.graphics.Color import android.hardware.SensorManager import androidx.core.graphics.Insets -import android.os.Looper -import android.os.StrictMode import android.view.View import android.widget.TextView import androidx.activity.ComponentActivity @@ -20,7 +18,6 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.children import com.google.android.material.appbar.AppBarLayout import com.google.android.material.appbar.CollapsingToolbarLayout -import uk.akane.omni.BuildConfig @Suppress("NOTHING_TO_INLINE") inline fun Context.doIHavePermission(perm: String) = @@ -106,21 +103,6 @@ fun SensorManager.checkSensorAvailability(sensorType: Int): Boolean { return getDefaultSensor(sensorType) != null } -// the whole point of this function is to do literally nothing at all (but without impacting -// performance) in release builds and ignore StrictMode violations in debug builds -inline fun allowDiskAccessInStrictMode(doIt: () -> T): T { - return if (BuildConfig.DEBUG) { - if (Looper.getMainLooper() != Looper.myLooper()) throw IllegalStateException() - val policy = StrictMode.allowThreadDiskReads() - try { - StrictMode.allowThreadDiskWrites() - doIt() - } finally { - StrictMode.setThreadPolicy(policy) - } - } else doIt() -} - fun View.enableEdgeToEdgePaddingListener(ime: Boolean = false, top: Boolean = false, extra: ((Insets) -> Unit)? = null) { if (fitsSystemWindows) throw IllegalArgumentException("must have fitsSystemWindows disabled") @@ -186,4 +168,8 @@ fun View.enableEdgeToEdgePaddingListener(ime: Boolean = false, top: Boolean = fa @Suppress("NOTHING_TO_INLINE") inline fun Int.dpToPx(context: Context): Int = - (this.toFloat() * context.resources.displayMetrics.density).toInt() \ No newline at end of file + (this.toFloat() * context.resources.displayMetrics.density).toInt() + +@Suppress("NOTHING_TO_INLINE") +inline fun Float.dpToPx(context: Context): Float = + (this * context.resources.displayMetrics.density) \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/logic/services/CompassTileService.kt b/app/src/main/java/uk/akane/omni/logic/services/CompassTileService.kt new file mode 100644 index 0000000..5211364 --- /dev/null +++ b/app/src/main/java/uk/akane/omni/logic/services/CompassTileService.kt @@ -0,0 +1,150 @@ +package uk.akane.omni.logic.services + +import android.annotation.SuppressLint +import android.app.NotificationChannel +import android.app.NotificationManager +import android.graphics.Bitmap +import android.graphics.Canvas +import android.graphics.Color +import android.graphics.PorterDuff +import android.graphics.drawable.Drawable +import android.graphics.drawable.Icon +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.service.quicksettings.Tile +import android.service.quicksettings.TileService +import android.util.Log +import android.view.Surface +import androidx.appcompat.content.res.AppCompatResources +import androidx.core.app.NotificationCompat +import androidx.core.content.ContextCompat +import androidx.core.content.getSystemService +import uk.akane.omni.R +import kotlin.math.absoluteValue + +class CompassTileService : TileService(), SensorEventListener { + + private val sensorManager + get() = getSystemService() + private val rotationVectorSensor + get() = sensorManager?.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + private val notificationManager + get() = getSystemService() + private lateinit var rotationIcon: Drawable + private lateinit var iconBitmap: Bitmap + + + companion object { + const val CHANNEL_ID = "COMPASS_CHANNEL" + const val NOTIFICATION_ID = 1 + } + + override fun onCreate() { + super.onCreate() + notificationManager?.createNotificationChannel( + NotificationChannel( + CHANNEL_ID, + getString(R.string.compass_tile_notification_channel), + NotificationManager.IMPORTANCE_LOW + ) + ) + rotationIcon = AppCompatResources.getDrawable(this, R.drawable.ic_pointer)!! + iconBitmap = Bitmap.createBitmap( + rotationIcon.intrinsicWidth, rotationIcon.intrinsicHeight, + Bitmap.Config.ARGB_8888 + ) + } + + override fun onClick() { + super.onClick() + Log.d("TAG", "onClick") + qsTile.state = when (qsTile.state) { + Tile.STATE_ACTIVE -> Tile.STATE_INACTIVE + Tile.STATE_INACTIVE -> Tile.STATE_ACTIVE + else -> Tile.STATE_INACTIVE + } + if (qsTile.state == Tile.STATE_INACTIVE) { + qsTile.label = getString(R.string.compass) + } + qsTile.updateTile() + } + + override fun onStartListening() { + super.onStartListening() + Log.d("TAG", "START LISTENING") + if (qsTile.state == Tile.STATE_ACTIVE) { + startForeground( + NOTIFICATION_ID, + NotificationCompat.Builder(this, CHANNEL_ID).setSmallIcon(R.drawable.ic_explorer) + .setContentTitle(getString(R.string.compass_notification_title)) + .setContentText(getString(R.string.compass_notification_label)) + .build() + ) + sensorManager?.registerListener( + this, + rotationVectorSensor, + SensorManager.SENSOR_DELAY_FASTEST + ) + } + } + + override fun onStopListening() { + super.onStopListening() + if (qsTile.state == Tile.STATE_ACTIVE) { + sensorManager?.unregisterListener(this) + stopForeground(STOP_FOREGROUND_REMOVE) + } + Log.d("TAG", "STOP LISTENING") + } + + override fun onSensorChanged(event: SensorEvent) { + if (event.sensor.type == Sensor.TYPE_ROTATION_VECTOR && qsTile.state == Tile.STATE_ACTIVE) { + updateCompass(event) + } + } + + @SuppressLint("StringFormatMatches") + private fun updateCompass(event: SensorEvent) { + val rotationVector = event.values.take(3).toFloatArray() + val rotationMatrix = FloatArray(9) + SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector) + + val displayRotation = ContextCompat.getDisplayOrDefault(baseContext).rotation + val remappedRotationMatrix = remapRotationMatrix(rotationMatrix, displayRotation) + + val orientationInRadians = FloatArray(3) + SensorManager.getOrientation(remappedRotationMatrix, orientationInRadians) + + val azimuthInDegrees = Math.toDegrees(orientationInRadians[0].toDouble()).toFloat() + val adjustedAzimuth = (azimuthInDegrees + 360) % 360 + + Canvas(iconBitmap).apply { + drawColor(Color.BLACK, PorterDuff.Mode.CLEAR) // clear all + rotate(-adjustedAzimuth, width / 2f, height / 2f) + rotationIcon.setBounds(0, 0, width, height) + rotationIcon.draw(this) + } + + qsTile.label = getString(R.string.degree_format_tile, adjustedAzimuth.toInt().absoluteValue) + qsTile.icon = Icon.createWithBitmap(iconBitmap) + + qsTile.updateTile() + } + + private fun remapRotationMatrix(rotationMatrix: FloatArray, displayRotation: Int): FloatArray { + val (newX, newY) = when (displayRotation) { + Surface.ROTATION_90 -> Pair(SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X) + Surface.ROTATION_180 -> Pair(SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y) + Surface.ROTATION_270 -> Pair(SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X) + else -> Pair(SensorManager.AXIS_X, SensorManager.AXIS_Y) + } + + val remappedRotationMatrix = FloatArray(9) + SensorManager.remapCoordinateSystem(rotationMatrix, newX, newY, remappedRotationMatrix) + return remappedRotationMatrix + } + + override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) = Unit +} diff --git a/app/src/main/java/uk/akane/omni/ui/MainActivity.kt b/app/src/main/java/uk/akane/omni/ui/MainActivity.kt index f657a04..37f5818 100644 --- a/app/src/main/java/uk/akane/omni/ui/MainActivity.kt +++ b/app/src/main/java/uk/akane/omni/ui/MainActivity.kt @@ -1,12 +1,17 @@ package uk.akane.omni.ui import android.os.Bundle +import android.util.Log +import android.view.WindowManager import androidx.appcompat.app.AppCompatActivity import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.fragment.app.Fragment import androidx.fragment.app.commit import uk.akane.omni.R import uk.akane.omni.logic.enableEdgeToEdgeProperly +import uk.akane.omni.ui.fragments.FlashlightFragment +import uk.akane.omni.ui.fragments.LevelFragment +import uk.akane.omni.ui.fragments.RulerFragment class MainActivity : AppCompatActivity() { @@ -17,6 +22,27 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) enableEdgeToEdgeProperly() setContentView(R.layout.activity_main) + window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON) + + Log.d("TAG", "YES:: ${intent.extras}") + if (intent.hasExtra("targetFragment")) { + intent.getIntExtra("targetFragment", 0).let { + when (it) { + 1 -> { + startFragment(LevelFragment()) + postComplete() + } + 2 -> { + startFragment(RulerFragment()) + postComplete() + } + 3 -> { + startFragment(FlashlightFragment()) + postComplete() + } + } + } + } } fun postComplete() = run { ready = true } diff --git a/app/src/main/java/uk/akane/omni/ui/components/CompassView.kt b/app/src/main/java/uk/akane/omni/ui/components/CompassView.kt index 6245e2e..d00a956 100644 --- a/app/src/main/java/uk/akane/omni/ui/components/CompassView.kt +++ b/app/src/main/java/uk/akane/omni/ui/components/CompassView.kt @@ -20,13 +20,13 @@ class CompassView @JvmOverloads constructor( init { inflate(context, R.layout.compass_layout, this) - degreeIndicatorTextView = findViewById(R.id.degree_indicator) + degreeIndicatorTextView = findViewById(R.id.degree_indicator)!! directionTextViews = listOf( - findViewById(R.id.north), findViewById(R.id.east), findViewById(R.id.south), findViewById(R.id.west), - findViewById(R.id.direction_1), findViewById(R.id.direction_2), findViewById(R.id.direction_3), - findViewById(R.id.direction_4), findViewById(R.id.direction_5), findViewById(R.id.direction_6), - findViewById(R.id.direction_7), findViewById(R.id.direction_8), findViewById(R.id.direction_9), - findViewById(R.id.direction_10), findViewById(R.id.direction_11), findViewById(R.id.direction_12), + findViewById(R.id.north)!!, findViewById(R.id.east)!!, findViewById(R.id.south)!!, findViewById(R.id.west)!!, + findViewById(R.id.direction_1)!!, findViewById(R.id.direction_2)!!, findViewById(R.id.direction_3)!!, + findViewById(R.id.direction_4)!!, findViewById(R.id.direction_5)!!, findViewById(R.id.direction_6)!!, + findViewById(R.id.direction_7)!!, findViewById(R.id.direction_8)!!, findViewById(R.id.direction_9)!!, + findViewById(R.id.direction_10)!!, findViewById(R.id.direction_11)!!, findViewById(R.id.direction_12)!!, degreeIndicatorTextView ) } diff --git a/app/src/main/java/uk/akane/omni/ui/components/RulerView.kt b/app/src/main/java/uk/akane/omni/ui/components/RulerView.kt new file mode 100644 index 0000000..818c4cb --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/components/RulerView.kt @@ -0,0 +1,85 @@ +package uk.akane.omni.ui.components + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import uk.akane.omni.logic.dpToPx +import com.google.android.material.color.MaterialColors +import uk.akane.omni.R + +class RulerView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val paintText = Paint().apply { + color = MaterialColors.getColor(this@RulerView, com.google.android.material.R.attr.colorOutline) + strokeWidth = 2f.dpToPx(context) + textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20f, resources.displayMetrics) + typeface = resources.getFont(R.font.hgm) + isAntiAlias = true + } + + private val paintMain = Paint().apply { + color = MaterialColors.getColor(this@RulerView, com.google.android.material.R.attr.colorOutline) + strokeWidth = 2f.dpToPx(context) + isAntiAlias = true + } + + private val paintSide = Paint().apply { + color = MaterialColors.getColor(this@RulerView, com.google.android.material.R.attr.colorOutline) + alpha = 127 + strokeWidth = 2f.dpToPx(context) + isAntiAlias = true + } + + // Calculate 1mm in pixels + private val mmToPx: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_MM, 1f, resources.displayMetrics) + private val topPadding: Float = 24f.dpToPx(context) + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val width = width.toFloat() + val height = height.toFloat() + + val numDivisions = ((height - topPadding) / mmToPx).toInt() + val longLineLength = width * 0.53f + val midLineLength = width * 0.43f + val shortLineLength = width * 0.34f + + for (i in 0..numDivisions) { + val y = topPadding + i * mmToPx + when { + i % 10 == 0 -> { + // Draw longer lines and numbers for every 10mm (1cm) + canvas.drawLine(width - longLineLength, y, width, y, paintMain) + val text = (i / 10).toString() + val textWidth = paintText.measureText(text) + val textHeight = paintText.descent() - paintText.ascent() + val textX = (width - longLineLength) / 2 - textWidth / 2 + val textY = y + textHeight / 3 + paintText.color = MaterialColors.getColor(this@RulerView, + if ((i / 10) % 5 == 0) + com.google.android.material.R.attr.colorOnSurface + else + com.google.android.material.R.attr.colorOutline + ) + canvas.drawText(text, textX, textY, paintText) + } + i % 5 == 0 -> { + // Draw medium lines for every 5mm (0.5cm) + canvas.drawLine(width - midLineLength, y, width, y, paintSide) + } + else -> { + // Draw shorter lines for other millimeters + canvas.drawLine(width - shortLineLength, y, width, y, paintSide) + } + } + } + } +} diff --git a/app/src/main/java/uk/akane/omni/ui/components/RulerViewInch.kt b/app/src/main/java/uk/akane/omni/ui/components/RulerViewInch.kt new file mode 100644 index 0000000..a7faec5 --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/components/RulerViewInch.kt @@ -0,0 +1,90 @@ +package uk.akane.omni.ui.components + +import android.content.Context +import android.graphics.Canvas +import android.graphics.Paint +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import uk.akane.omni.logic.dpToPx +import com.google.android.material.color.MaterialColors +import uk.akane.omni.R + +class RulerViewInch @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private val paintText = Paint().apply { + color = MaterialColors.getColor(this@RulerViewInch, com.google.android.material.R.attr.colorOutline) + strokeWidth = 2f.dpToPx(context) + textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 20f, resources.displayMetrics) + typeface = resources.getFont(R.font.hgm) + isAntiAlias = true + } + + private val paintMain = Paint().apply { + color = MaterialColors.getColor(this@RulerViewInch, com.google.android.material.R.attr.colorOutline) + strokeWidth = 2f.dpToPx(context) + isAntiAlias = true + } + + private val paintSide = Paint().apply { + color = MaterialColors.getColor(this@RulerViewInch, com.google.android.material.R.attr.colorOutline) + alpha = 127 + strokeWidth = 2f.dpToPx(context) + isAntiAlias = true + } + + // Calculate 1 inch in pixels + private val inchToPx: Float = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_IN, 1f, resources.displayMetrics) + private val inchInterval: Float = inchToPx / 10f // 1 inch is divided into 10 intervals + private val inchTextInterval: Int = 10 // Show text for every 10 intervals (1 inch) + private val topPadding: Float = 24f.dpToPx(context) + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + val width = width.toFloat() + val height = height.toFloat() + + val numInches = (height - topPadding) / inchToPx + val numIntervals = numInches * 10 + + val longLineLength = width * 0.53f + val midLineLength = width * 0.43f + val shortLineLength = width * 0.34f + + for (i in 0..numIntervals.toInt()) { + val y = topPadding + i * inchInterval + when { + i % inchTextInterval == 0 -> { + // Draw longer lines and numbers for every inch + canvas.drawLine(0f, y, longLineLength, y, paintMain) + val text = (i / inchTextInterval).toString() + val textWidth = paintText.measureText(text) + val textHeight = paintText.descent() - paintText.ascent() + val textX = (width - longLineLength) / 2 + longLineLength - textWidth / 2 + + val textY = y + textHeight / 3 + paintText.color = MaterialColors.getColor(this@RulerViewInch, + if ((i / inchTextInterval) % 12 == 0) + com.google.android.material.R.attr.colorOnSurface + else + com.google.android.material.R.attr.colorOutline + ) + canvas.drawText(text, textX, textY, paintText) + } + i % 5 == 0 -> { + // Draw medium lines for every 5 intervals (0.5 inch) + canvas.drawLine(0f, y, midLineLength, y, paintSide) + } + else -> { + // Draw shorter lines for other intervals + canvas.drawLine(0f, y, shortLineLength, y, paintSide) + } + } + } + } +} diff --git a/app/src/main/java/uk/akane/omni/ui/components/SpiritLevelView.kt b/app/src/main/java/uk/akane/omni/ui/components/SpiritLevelView.kt new file mode 100644 index 0000000..106936c --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/components/SpiritLevelView.kt @@ -0,0 +1,241 @@ +package uk.akane.omni.ui.components + +import android.content.Context +import android.graphics.* +import android.util.AttributeSet +import android.util.TypedValue +import android.view.View +import androidx.core.content.res.ResourcesCompat +import uk.akane.omni.R +import uk.akane.omni.logic.dpToPx +import com.google.android.material.color.MaterialColors +import kotlin.math.absoluteValue +import kotlin.math.max +import kotlin.math.min +import kotlin.math.pow +import kotlin.math.sqrt + +class SpiritLevelView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : View(context, attrs, defStyleAttr) { + + private var pitch: Float = 0f + private var roll: Float = 0f + private var balance: Float = 0f + private var pitchAngle: Float = 0f + + private val containerPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + } + + private val levelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + } + + private val textPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + textSize = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_SP, 42f, resources.displayMetrics) + textAlign = Paint.Align.CENTER + typeface = resources.getFont(R.font.hgm) + } + + private val outerLevelPaint = Paint(Paint.ANTI_ALIAS_FLAG).apply { + style = Paint.Style.FILL + xfermode = PorterDuffXfermode(PorterDuff.Mode.SRC_IN) + } + + private var colorPrimary: Int = 0 + private var colorOnPrimary: Int = 0 + private var colorTertiary: Int = 0 + private var colorOnTertiary: Int = 0 + private var colorSurface: Int = 0 + private var colorOnSurface: Int = 0 + private var colorPrimaryContainer: Int = 0 + + private val leftPolygon = ResourcesCompat.getDrawable(resources, R.drawable.ic_polygon_left, null)!! + private val rightPolygon = ResourcesCompat.getDrawable(resources, R.drawable.ic_polygon_right, null)!! + private val polygonHeight = 51.dpToPx(context) + private val polygonWidth = 50.dpToPx(context) + private val sideMargin = 16.dpToPx(context) + private val roundCorner = 16f.dpToPx(context) + + init { + colorPrimary = MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimary) + colorOnPrimary = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnPrimary) + colorTertiary = MaterialColors.getColor(this, com.google.android.material.R.attr.colorTertiary) + colorOnTertiary = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnTertiary) + colorSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorSurface) + colorOnSurface = MaterialColors.getColor(this, com.google.android.material.R.attr.colorOnSurface) + colorPrimaryContainer = MaterialColors.getColor(this, com.google.android.material.R.attr.colorPrimaryContainer) + containerPaint.color = colorSurface + textPaint.color = colorOnSurface + } + + private var levelRadius: Float = 0f + private var translationRange: Float = 0f + private var directionalLength: Float = 0f + private var firstTransformDegree: Float = 0f + private var transformFactor: Float = 0f + + override fun onSizeChanged(w: Int, h: Int, oldw: Int, oldh: Int) { + super.onSizeChanged(w, h, oldw, oldh) + levelRadius = w / 2 * 0.36f + translationRange = max(w, h).toFloat() + directionalLength = sqrt((w.toFloat().pow(2)) + (h.toFloat().pow(2))) / 2 + val topLeft = (height - polygonHeight) / 2 + leftPolygon.setBounds( + sideMargin, + topLeft, + sideMargin + polygonWidth, + topLeft + polygonHeight + ) + rightPolygon.setBounds( + width - sideMargin - polygonWidth, + (height - polygonHeight) / 2, + width - sideMargin, + (height - polygonHeight) / 2 + polygonHeight + ) + } + + override fun onDraw(canvas: Canvas) { + super.onDraw(canvas) + + // Calculates indicator center location. + val cx = width / 2f + val cy = height / 2f + + // Calculates horizontal indicator position. + val levelCx = cx + (roll / 90) * translationRange + val levelCy = cy - (pitch / 90) * translationRange + val offScreenProgress = sqrt( + (((cy - levelCy).absoluteValue).pow(2) + ((cx - levelCx).absoluteValue).pow(2)) + ) - levelRadius + + if (pitch.absoluteValue > 2f || roll.absoluteValue > 2f) { + levelPaint.color = colorPrimary + outerLevelPaint.color = colorOnPrimary + } else { + levelPaint.color = colorTertiary + outerLevelPaint.color = colorOnTertiary + } + + drawHorizontalIndicators(cx, cy, levelCx, levelCy, canvas) + drawVerticalLayer(canvas, balance, offScreenProgress) + + val saveCount = canvas.saveLayer(null, null) + + drawCenteredText(cx, cy, canvas, balance, offScreenProgress) + drawInvertedTextLayer(levelCx, levelCy, canvas) + drawRoundedRect(canvas, transformFactor, balance, outerLevelPaint) + + canvas.restoreToCount(saveCount) + } + + private fun drawHorizontalIndicators( + cx: Float, + cy: Float, + levelCx: Float, + levelCy: Float, + canvas: Canvas + ) { + // Draws container radius. + val bigCircleRadius = sqrt( + (((cy - levelCy).absoluteValue).pow(2) + ((cx - levelCx).absoluteValue).pow(2)) + ) + levelRadius + + // Draws indicators + canvas.drawCircle(cx, cy, bigCircleRadius, containerPaint) + canvas.drawCircle(levelCx, levelCy, levelRadius, levelPaint) + } + + private fun drawVerticalLayer( + canvas: Canvas, + angle: Float, + offScreenProgress: Float + ) { + if (offScreenProgress > directionalLength) { + if (firstTransformDegree == 0f) firstTransformDegree = pitchAngle + transformFactor = (pitchAngle - firstTransformDegree) / 5f + if (angle.absoluteValue > 2f && angle !in 178f .. 182f) { + leftPolygon.setTint(colorPrimaryContainer) + rightPolygon.setTint(colorPrimaryContainer) + } else { + leftPolygon.setTint(colorTertiary) + rightPolygon.setTint(colorTertiary) + } + if (transformFactor in 0f .. 1f) { + leftPolygon.alpha = (transformFactor * 255).toInt() + rightPolygon.alpha = (transformFactor * 255).toInt() + drawRoundedRect(canvas, transformFactor, angle, levelPaint) + containerPaint.alpha = ((1f - transformFactor) * 255).toInt() + } else if (transformFactor > 1f) { + leftPolygon.alpha = 255 + rightPolygon.alpha = 255 + drawRoundedRect(canvas, 1f, angle, levelPaint) + containerPaint.alpha = 0 + } + leftPolygon.draw(canvas) + rightPolygon.draw(canvas) + } else { + return + } + } + + private fun drawRoundedRect( + canvas: Canvas, + transformValue: Float, + angle: Float, + paint: Paint + ) { + canvas.save() + canvas.rotate(angle, width / 2f, height / 2f) + canvas.drawRoundRect( + 0f, + height - (height / 2 * min(transformValue, 1f)), + width.toFloat(), + height.toFloat() * 2, + roundCorner, + roundCorner, + paint + ) + canvas.restore() + } + + private fun drawCenteredText( + cx: Float, + cy: Float, + canvas: Canvas, + textAngle: Float, + offScreenProgress: Float + ) { + canvas.save() + canvas.rotate(textAngle, cx, cy) + val text = + if (offScreenProgress < directionalLength) + " ${pitchAngle.toInt().absoluteValue}°" + else if (offScreenProgress > directionalLength && pitch > 0) + " ${(- balance + 180f).toInt().absoluteValue}°" + else + " ${textAngle.toInt().absoluteValue}°" + canvas.drawText(text, cx, cy + (textPaint.textSize / 4), textPaint) + canvas.restore() + } + + private fun drawInvertedTextLayer( + levelCx: Float, + levelCy: Float, + canvas: Canvas + ) { + // Draws an overlay layer for the inverted text color. + canvas.drawCircle(levelCx, levelCy, levelRadius, outerLevelPaint) + } + + fun updatePitchAndRollAndBalance(pitch: Float, roll: Float, balance: Float) { + this.pitch = pitch + this.roll = roll + this.balance = if (pitch < 0) balance else 180f - balance + this.pitchAngle = sqrt(pitch.absoluteValue.pow(2) + roll.absoluteValue.pow(2)) + invalidate() + } +} diff --git a/app/src/main/java/uk/akane/omni/ui/components/SwitchBottomSheet.kt b/app/src/main/java/uk/akane/omni/ui/components/SwitchBottomSheet.kt new file mode 100644 index 0000000..e406d0e --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/components/SwitchBottomSheet.kt @@ -0,0 +1,113 @@ +package uk.akane.omni.ui.components + +import android.animation.ValueAnimator +import android.content.Context +import android.content.res.ColorStateList +import android.os.Bundle +import android.util.Log +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.fragment.app.commit +import uk.akane.omni.R +import uk.akane.omni.ui.fragments.CompassFragment +import uk.akane.omni.ui.fragments.FlashlightFragment +import uk.akane.omni.ui.fragments.LevelFragment +import uk.akane.omni.ui.fragments.RulerFragment +import com.google.android.material.bottomsheet.BottomSheetDialogFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.color.MaterialColors +import com.google.android.material.color.utilities.ColorUtils + +class SwitchBottomSheet( + private val callFragmentType : CallFragmentType +) : BottomSheetDialogFragment() { + + enum class CallFragmentType { + COMPASS, + SPIRIT_LEVEL, + BAROMETER, + RULER, + FLASHLIGHT + } + + private lateinit var compassMaterialButton: MaterialButton + private lateinit var spiritLevelMaterialButton: MaterialButton + private lateinit var barometerMaterialButton: MaterialButton + private lateinit var rulerMaterialButton: MaterialButton + private lateinit var flashlightMaterialButton: MaterialButton + + private lateinit var targetMaterialButton: MaterialButton + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.switch_bottom_sheet, container, false) + + compassMaterialButton = rootView.findViewById(R.id.compass_btn)!! + spiritLevelMaterialButton = rootView.findViewById(R.id.spirit_leveler_btn)!! + barometerMaterialButton = rootView.findViewById(R.id.barometer_btn)!! + rulerMaterialButton = rootView.findViewById(R.id.ruler_btn)!! + flashlightMaterialButton = rootView.findViewById(R.id.flashlight_btn)!! + + targetMaterialButton = when (callFragmentType) { + CallFragmentType.COMPASS -> compassMaterialButton + CallFragmentType.SPIRIT_LEVEL -> spiritLevelMaterialButton + CallFragmentType.BAROMETER -> barometerMaterialButton + CallFragmentType.RULER -> rulerMaterialButton + CallFragmentType.FLASHLIGHT -> flashlightMaterialButton + } + + targetMaterialButton.isChecked = true + + setOnClickListener() + + return rootView + } + + private fun setOnClickListener() { + compassMaterialButton.setOnClickListener { + if (targetMaterialButton != compassMaterialButton) { + val fm = requireActivity().supportFragmentManager + fm.commit { + hide(fm.fragments.last()) + replace(R.id.container, CompassFragment()) + } + dismiss() + } + } + spiritLevelMaterialButton.setOnClickListener { + if (targetMaterialButton != spiritLevelMaterialButton) { + val fm = requireActivity().supportFragmentManager + fm.commit { + hide(fm.fragments.last()) + replace(R.id.container, LevelFragment()) + } + dismiss() + } + } + rulerMaterialButton.setOnClickListener { + if (targetMaterialButton != rulerMaterialButton) { + val fm = requireActivity().supportFragmentManager + fm.commit { + hide(fm.fragments.last()) + replace(R.id.container, RulerFragment()) + } + dismiss() + } + } + flashlightMaterialButton.setOnClickListener { + if (targetMaterialButton != flashlightMaterialButton) { + val fm = requireActivity().supportFragmentManager + fm.commit { + hide(fm.fragments.last()) + replace(R.id.container, FlashlightFragment()) + } + dismiss() + } + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/BaseFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/BaseFragment.kt index 921f701..1362520 100644 --- a/app/src/main/java/uk/akane/omni/ui/fragments/BaseFragment.kt +++ b/app/src/main/java/uk/akane/omni/ui/fragments/BaseFragment.kt @@ -11,17 +11,17 @@ abstract class BaseFragment : Fragment() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // Enable material transitions. - enterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true) - returnTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false) - exitTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ true) - reenterTransition = MaterialSharedAxis(MaterialSharedAxis.X, /* forward= */ false) + enterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, /* forward= */ true) + returnTransition = MaterialSharedAxis(MaterialSharedAxis.Z, /* forward= */ false) + exitTransition = MaterialSharedAxis(MaterialSharedAxis.Z, /* forward= */ true) + reenterTransition = MaterialSharedAxis(MaterialSharedAxis.Z, /* forward= */ false) } // https://github.com/material-components/material-components-android/issues/1984#issuecomment-1089710991 override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) // Overlap colors. - view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.colorBackground)) + view.setBackgroundColor(MaterialColors.getColor(view, com.google.android.material.R.attr.colorSurfaceContainer)) } } \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/BasePreferenceFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/BasePreferenceFragment.kt index a44dc64..c898c30 100644 --- a/app/src/main/java/uk/akane/omni/ui/fragments/BasePreferenceFragment.kt +++ b/app/src/main/java/uk/akane/omni/ui/fragments/BasePreferenceFragment.kt @@ -10,8 +10,6 @@ import androidx.preference.PreferenceFragmentCompat import androidx.recyclerview.widget.RecyclerView import com.google.android.material.color.MaterialColors import uk.akane.omni.R -import uk.akane.omni.logic.allowDiskAccessInStrictMode -import uk.akane.omni.logic.dpToPx import uk.akane.omni.logic.enableEdgeToEdgePaddingListener /** @@ -26,15 +24,8 @@ abstract class BasePreferenceFragment : PreferenceFragmentCompat(), override fun onViewCreated(view: View, savedInstanceState: Bundle?) { super.onViewCreated(view, savedInstanceState) - view.setBackgroundColor(MaterialColors.getColor(view, android.R.attr.colorBackground)) - view.findViewById(androidx.preference.R.id.recycler_view).apply { - setPadding(paddingLeft, paddingTop + 12.dpToPx(context), paddingRight, paddingBottom) - enableEdgeToEdgePaddingListener() - } - } - - override fun setPreferencesFromResource(preferencesResId: Int, key: String?) { - allowDiskAccessInStrictMode { super.setPreferencesFromResource(preferencesResId, key) } + view.setBackgroundColor(MaterialColors.getColor(view, com.google.android.material.R.attr.colorSurfaceContainer)) + view.findViewById(androidx.preference.R.id.recycler_view)!!.enableEdgeToEdgePaddingListener() } override fun setDivider(divider: Drawable?) { diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/BaseSettingFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/BaseSettingFragment.kt index 5ee6921..115e55d 100644 --- a/app/src/main/java/uk/akane/omni/ui/fragments/BaseSettingFragment.kt +++ b/app/src/main/java/uk/akane/omni/ui/fragments/BaseSettingFragment.kt @@ -19,12 +19,10 @@ abstract class BaseSettingFragment(private val str: Int, savedInstanceState: Bundle?, ): View? { val rootView = inflater.inflate(R.layout.fragment_top_settings, container, false) - val topAppBar = rootView.findViewById(R.id.topAppBar) - val collapsingToolbar = - rootView.findViewById(R.id.collapsingtoolbar) + val topAppBar = rootView.findViewById(R.id.topAppBar)!! - rootView.findViewById(R.id.appbarlayout).enableEdgeToEdgePaddingListener() - collapsingToolbar.title = getString(str) + rootView.findViewById(R.id.appbarlayout)!!.enableEdgeToEdgePaddingListener() + topAppBar.title = getString(str) topAppBar.setNavigationOnClickListener { requireActivity().supportFragmentManager.popBackStack() diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/CompassFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/CompassFragment.kt index 9bc803c..cdcbdf1 100644 --- a/app/src/main/java/uk/akane/omni/ui/fragments/CompassFragment.kt +++ b/app/src/main/java/uk/akane/omni/ui/fragments/CompassFragment.kt @@ -1,6 +1,8 @@ package uk.akane.omni.ui.fragments import android.Manifest +import android.content.SharedPreferences +import android.hardware.GeomagneticField import android.hardware.Sensor import android.hardware.SensorEvent import android.hardware.SensorEventListener @@ -28,25 +30,29 @@ import androidx.core.view.WindowInsetsCompat import androidx.core.view.updateLayoutParams import androidx.fragment.app.activityViewModels import androidx.lifecycle.coroutineScope +import androidx.preference.PreferenceManager import com.google.android.material.button.MaterialButton import com.google.android.material.dialog.MaterialAlertDialogBuilder import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import uk.akane.omni.R import uk.akane.omni.logic.checkSensorAvailability +import uk.akane.omni.logic.doIHavePermission import uk.akane.omni.logic.fadInAnimation import uk.akane.omni.logic.fadOutAnimation import uk.akane.omni.logic.isLocationPermissionGranted import uk.akane.omni.logic.setTextAnimation import uk.akane.omni.ui.MainActivity import uk.akane.omni.ui.components.CompassView +import uk.akane.omni.ui.components.SwitchBottomSheet import uk.akane.omni.ui.fragments.settings.MainSettingsFragment import uk.akane.omni.ui.viewmodels.OmniViewModel import java.util.Locale import kotlin.math.abs -class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { +class CompassFragment : BaseFragment(), SensorEventListener, LocationListener, + SharedPreferences.OnSharedPreferenceChangeListener { private val omniViewModel: OmniViewModel by activityViewModels() @@ -57,8 +63,6 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { private var rotationVectorSensor: Sensor? = null - private var lastDegree = 0f - private lateinit var compassView: CompassView private lateinit var textIndicatorTextView: TextView private lateinit var sheetMaterialButton: MaterialButton @@ -75,8 +79,13 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { private lateinit var requestPermissionLauncher: ActivityResultLauncher + private lateinit var prefs: SharedPreferences + private var isAnimating: Boolean = false private var doNotHaveSensor: Boolean = false + private var hapticFeedback: Boolean = true + private var trueNorth: Boolean = false + private var useDms: Boolean = false override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) @@ -90,8 +99,6 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { if (!sensorManager!!.checkSensorAvailability(Sensor.TYPE_ROTATION_VECTOR)) { mainActivity!!.postComplete() doNotHaveSensor = true - } else { - sensorManager!!.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST) } geocoder = Geocoder(requireContext(), Locale.getDefault()) @@ -104,14 +111,18 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { registerForActivityResult( ActivityResultContracts.RequestPermission() ) { isGranted: Boolean -> - if (isGranted) { + if (isGranted && requireContext().isLocationPermissionGranted) { requestLocationUpdates() showLongitudeLatitude() } } - mutableListOf(1, 2, 3).sort() + prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + prefs.registerOnSharedPreferenceChangeListener(this) + hapticFeedback = prefs.getBoolean("haptic_feedback", true) + trueNorth = prefs.getBoolean("true_north", false) + useDms = prefs.getBoolean("coordinate", false) } private fun requestLocationUpdates() { @@ -132,13 +143,44 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { } private fun updateLocationStatus(location: Location) { - latitudeTextView.text = String.format(Locale.getDefault(), "%.4f", location.latitude) - longitudeTextView.text = String.format(Locale.getDefault(), "%.4f", location.longitude) + if (!useDms) { + latitudeTextView.text = String.format(Locale.getDefault(), "%.4f", location.latitude) + longitudeTextView.text = String.format(Locale.getDefault(), "%.4f", location.longitude) + } else { + val listLatitudeDMS = Location.convert(location.latitude, Location.FORMAT_SECONDS).split(':') + val longitudeDMS = Location.convert(location.longitude, Location.FORMAT_SECONDS).split(':') + val regex = """[.,٫]\d+""".toRegex() + latitudeTextView.text = getString( + R.string.dms_format, + listLatitudeDMS[0], + listLatitudeDMS[1], + listLatitudeDMS[2].replace(regex, "") + ) + longitudeTextView.text = getString( + R.string.dms_format, + longitudeDMS[0], + longitudeDMS[1], + longitudeDMS[2].replace(regex, "") + ) + } updateCity(location) } - override fun onDestroy() { + override fun onResume() { + super.onResume() + if (sensorManager!!.checkSensorAvailability(Sensor.TYPE_ROTATION_VECTOR)) { + sensorManager!!.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST) + } + } + + + override fun onPause() { + super.onPause() sensorManager!!.unregisterListener(this) + } + + override fun onDestroy() { + prefs.unregisterOnSharedPreferenceChangeListener(this) super.onDestroy() } @@ -149,16 +191,16 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { ): View? { val rootView = inflater.inflate(R.layout.fragment_compass, container, false) - compassView = rootView.findViewById(R.id.compass_view) - textIndicatorTextView = rootView.findViewById(R.id.text_indicator) - sheetMaterialButton = rootView.findViewById(R.id.sheet_btn) - settingsMaterialButton = rootView.findViewById(R.id.settings_btn) - latitudeTextView = rootView.findViewById(R.id.latitude) - longitudeTextView = rootView.findViewById(R.id.longitude) - latitudeDescTextView = rootView.findViewById(R.id.latitude_desc) - longitudeDescTextView = rootView.findViewById(R.id.longitude_desc) - cityTextView = rootView.findViewById(R.id.city) - notActiveMaterialButton = rootView.findViewById(R.id.not_available_btn) + compassView = rootView.findViewById(R.id.compass_view)!! + textIndicatorTextView = rootView.findViewById(R.id.text_indicator)!! + sheetMaterialButton = rootView.findViewById(R.id.sheet_btn)!! + settingsMaterialButton = rootView.findViewById(R.id.settings_btn)!! + latitudeTextView = rootView.findViewById(R.id.latitude)!! + longitudeTextView = rootView.findViewById(R.id.longitude)!! + latitudeDescTextView = rootView.findViewById(R.id.latitude_desc)!! + longitudeDescTextView = rootView.findViewById(R.id.longitude_desc)!! + cityTextView = rootView.findViewById(R.id.city)!! + notActiveMaterialButton = rootView.findViewById(R.id.not_available_btn)!! directionStringList = listOf( getString(R.string.north), @@ -187,6 +229,10 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { mainActivity!!.startFragment(MainSettingsFragment()) } + sheetMaterialButton.setOnClickListener { + SwitchBottomSheet(SwitchBottomSheet.CallFragmentType.COMPASS).show(parentFragmentManager, "switch_bottom_sheet") + } + omniViewModel.lastKnownLocation.value?.let { updateLocationStatus(it) } @@ -203,6 +249,27 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { .show() } + if (omniViewModel.lastDegree.value != null) { + updateCompassViewWithAzimuth(omniViewModel.lastDegree.value!!) + } + + if (!requireContext().doIHavePermission(Manifest.permission.POST_NOTIFICATIONS) && + !prefs.getBoolean("isPostNotificationPromptShown" , false) && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(resources.getString(R.string.compass_prompt_title)) + .setMessage(resources.getString(R.string.compass_prompt_desc)) + .setIcon(R.drawable.ic_notifications) + .setNegativeButton(resources.getString(R.string.decline)) { _, _ -> + prefs.edit().putBoolean("isPostNotificationPromptShown", true).apply() + } + .setPositiveButton(resources.getString(R.string.accept)) { _, _ -> + requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS) + prefs.edit().putBoolean("isPostNotificationPromptShown", true).apply() + } + .show() + } + return rootView } @@ -248,9 +315,24 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { val azimuthInDegrees = Math.toDegrees(azimuthInRadians.toDouble()).toFloat() val adjustedAzimuth = if (azimuthInDegrees < 0) 360f + azimuthInDegrees else azimuthInDegrees - if (lastDegree == 0f) lastDegree = adjustedAzimuth + if (omniViewModel.lastDegree.value == null) omniViewModel.setLastDegree(adjustedAzimuth) + + if (trueNorth && omniViewModel.lastKnownLocation.value != null) { + val magneticDeclination = getMagneticDeclination(omniViewModel.lastKnownLocation.value!!) + val trueAzimuth = adjustedAzimuth.plus(magneticDeclination) + updateCompassViewWithAzimuth(trueAzimuth) + } else { + updateCompassViewWithAzimuth(adjustedAzimuth) + } + } - updateCompassViewWithAzimuth(adjustedAzimuth) + private fun getMagneticDeclination(location: Location): Float { + val latitude = location.latitude.toFloat() + val longitude = location.longitude.toFloat() + val altitude = location.altitude.toFloat() + val time = location.time + val geomagneticField = GeomagneticField(latitude, longitude, altitude, time) + return geomagneticField.declination } private fun remapRotationMatrix(rotationMatrix: FloatArray, displayRotation: Int?): FloatArray { @@ -269,7 +351,9 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { private fun updateCompassViewWithAzimuth(azimuthInDegrees: Float) { compassView.rotate(-azimuthInDegrees) updateTextIndicatorWithAzimuth(azimuthInDegrees) - checkAndVibrate(azimuthInDegrees) + if (hapticFeedback) { + checkAndVibrate(azimuthInDegrees) + } } private fun updateTextIndicatorWithAzimuth(degree: Float) { @@ -279,9 +363,10 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { private fun checkAndVibrate(degree: Float) { val threshold = 2f - if (abs(degree - lastDegree) > threshold) { + if (omniViewModel.lastDegree.value != null && + abs(degree - omniViewModel.lastDegree.value!!) > threshold) { view?.performHapticFeedback(HapticFeedbackConstants.CLOCK_TICK) - lastDegree = degree + omniViewModel.setLastDegree(degree) } } @@ -394,4 +479,18 @@ class CompassFragment : BaseFragment(), SensorEventListener, LocationListener { val EXIT_INTERPOLATOR = PathInterpolator(0.3f, 0f, 1f, 1f) } + override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences?, key: String?) { + when (key) { + "haptic_feedback" -> { + hapticFeedback = prefs.getBoolean("haptic_feedback", true) + } + "true_north" -> { + trueNorth = prefs.getBoolean("true_north", false) + } + "coordinate" -> { + useDms = prefs.getBoolean("coordinate", false) + } + } + } + } \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/FlashlightFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/FlashlightFragment.kt new file mode 100644 index 0000000..a0944f2 --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/fragments/FlashlightFragment.kt @@ -0,0 +1,271 @@ +package uk.akane.omni.ui.fragments + +import android.content.SharedPreferences +import android.content.res.ColorStateList +import android.hardware.camera2.CameraCharacteristics +import android.hardware.camera2.CameraManager +import android.os.Build +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.RequiresApi +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import androidx.preference.PreferenceManager +import uk.akane.omni.R +import uk.akane.omni.ui.MainActivity +import uk.akane.omni.ui.components.SwitchBottomSheet +import uk.akane.omni.ui.fragments.settings.MainSettingsFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.color.MaterialColors +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import com.google.android.material.slider.Slider + + +class FlashlightFragment : BaseFragment() { + + private lateinit var sheetMaterialButton: MaterialButton + private lateinit var settingsMaterialButton: MaterialButton + private lateinit var flashlightSlider: Slider + private lateinit var cameraManager: CameraManager + private var keyCameraId: String? = null + private var maximumFlashlightLevel: Int? = null + private var notSupported: Boolean = true + private var isUserTouching: Boolean = false + private var maximumBrightnessLevelThreshold: Int = 0 + private var pastValue: Float = 0f + + private var mainActivity: MainActivity? = null + + private lateinit var prefs: SharedPreferences + + private var torchListener: CameraManager.TorchCallback = object : CameraManager.TorchCallback() { + override fun onTorchModeChanged(cameraId: String, enabled: Boolean) { + if (!isUserTouching && !notSupported && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + maximumFlashlightLevel != null && maximumFlashlightLevel!! > 1) { + flashlightSlider.value = if (enabled) cameraManager.getTorchStrengthLevel(cameraId) + .toFloat() else 0.0f + if (this@FlashlightFragment::flashlightSlider.isInitialized) { + switchTrackColor(flashlightSlider.value) + } + } else if ( + !isUserTouching && !notSupported && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + flashlightSlider.value = if (enabled) 1.0f else 0.0f + } + } + + override fun onTorchStrengthLevelChanged(cameraId: String, newStrengthLevel: Int) { + if (!isUserTouching && !notSupported && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU && + maximumFlashlightLevel != null && maximumFlashlightLevel!! > 1) { + flashlightSlider.value = cameraManager.getTorchStrengthLevel(cameraId).toFloat() + if (this@FlashlightFragment::flashlightSlider.isInitialized) { + switchTrackColor(flashlightSlider.value) + } + } else if ( + !isUserTouching && !notSupported && + Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + flashlightSlider.value = if (newStrengthLevel == 1) 1.0f else 0.0f + } + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mainActivity = requireActivity() as MainActivity + prefs = PreferenceManager.getDefaultSharedPreferences(requireContext()) + cameraManager = ContextCompat.getSystemService(requireContext(), CameraManager::class.java)!! + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + for (i in 0 until cameraManager.cameraIdList.size) { + val cameraCharacteristics = + cameraManager.getCameraCharacteristics(cameraManager.cameraIdList[i]) + + val isFlashlightAvailable = + cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_AVAILABLE) + maximumFlashlightLevel = + cameraCharacteristics.get(CameraCharacteristics.FLASH_INFO_STRENGTH_MAXIMUM_LEVEL) + + if (maximumFlashlightLevel != null && maximumFlashlightLevel!! > 1) { + maximumBrightnessLevelThreshold = + (maximumFlashlightLevel!! * 0.9).toInt() + } + + if (isFlashlightAvailable == true && maximumFlashlightLevel != null && maximumFlashlightLevel!! > 0) { + keyCameraId = cameraManager.cameraIdList[i] + notSupported = false + break + } + } + } + + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.fragment_flashlight, container, false) + + sheetMaterialButton = rootView.findViewById(R.id.sheet_btn)!! + settingsMaterialButton = rootView.findViewById(R.id.settings_btn)!! + + flashlightSlider = rootView.findViewById(R.id.flashlight_slider)!! + + settingsMaterialButton.setOnClickListener { + mainActivity!!.startFragment(MainSettingsFragment()) + } + + sheetMaterialButton.setOnClickListener { + SwitchBottomSheet(SwitchBottomSheet.CallFragmentType.FLASHLIGHT).show(parentFragmentManager, "switch_bottom_sheet") + } + + if (notSupported) { + flashlightSlider.isEnabled = false + } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + flashlightSlider.stepSize = 1.0f + flashlightSlider.valueTo = maximumFlashlightLevel!!.toFloat() + flashlightSlider.addOnChangeListener { _, value, _ -> + if (maximumBrightnessLevelThreshold > 0 && + value >= maximumBrightnessLevelThreshold && + !prefs.getBoolean("flashlight_acknowledged", false) + ) { + flashlightSlider.isEnabled = false + MaterialAlertDialogBuilder(requireContext()) + .setTitle(resources.getString(R.string.flashlight_dialog_title)) + .setMessage(resources.getString(R.string.flashlight_dialog_text)) + .setIcon(R.drawable.ic_warning) + .setNegativeButton(resources.getString(R.string.decline)) { _, _ -> + flashlightSlider.isEnabled = true + if (pastValue < maximumBrightnessLevelThreshold) { + flashlightSlider.value = pastValue + } else { + flashlightSlider.value = 0f + } + } + .setPositiveButton(resources.getString(R.string.accept)) { _, _ -> + flashlightSlider.isEnabled = true + prefs.edit() + .putBoolean("flashlight_acknowledged", true) + .apply() + turnOnTorch(value) + switchTrackColor(value) + } + .setOnDismissListener { + flashlightSlider.isEnabled = true + if (pastValue < maximumBrightnessLevelThreshold && + !prefs.getBoolean("flashlight_acknowledged", false)) { + flashlightSlider.value = pastValue + } else if (!prefs.getBoolean("flashlight_acknowledged", false)) { + flashlightSlider.value = 0f + } + } + .show() + return@addOnChangeListener + } + + switchTrackColor(value) + turnOnTorch(value) + + pastValue = value + } + flashlightSlider.addOnSliderTouchListener(object : Slider.OnSliderTouchListener { + override fun onStartTrackingTouch(slider: Slider) { + isUserTouching = true + } + + override fun onStopTrackingTouch(slider: Slider) { + isUserTouching = false + } + + }) + cameraManager.registerTorchCallback(torchListener, null) + } + return rootView + } + + @RequiresApi(Build.VERSION_CODES.TIRAMISU) + private fun turnOnTorch(value: Float) { + if (maximumFlashlightLevel == 1) { + cameraManager.setTorchMode( + keyCameraId!!, + if (value >= 0.5f) true else false + ) + } else if (value >= 0.5){ + cameraManager.turnOnTorchWithStrengthLevel(keyCameraId!!, value.toInt()) + } else { + cameraManager.setTorchMode(keyCameraId!!, false) + } + } + + private fun switchTrackColor(value: Float) { + if (value >= maximumBrightnessLevelThreshold && maximumBrightnessLevelThreshold > 0) { + setHighlightTrackColor() + } else if (maximumBrightnessLevelThreshold > 0) { + setDefaultTrackColor() + } + } + + private fun setHighlightTrackColor() { + flashlightSlider.thumbTintList = + ColorStateList.valueOf( + MaterialColors.getColor( + flashlightSlider, + com.google.android.material.R.attr.colorError + ) + ) + flashlightSlider.trackActiveTintList = + ColorStateList.valueOf( + MaterialColors.getColor( + flashlightSlider, + com.google.android.material.R.attr.colorError + ) + ) + } + + private fun setDefaultTrackColor() { + flashlightSlider.thumbTintList = + ColorStateList.valueOf( + MaterialColors.getColor( + flashlightSlider, + com.google.android.material.R.attr.colorPrimary + ) + ) + flashlightSlider.trackActiveTintList = + ColorStateList.valueOf( + MaterialColors.getColor( + flashlightSlider, + com.google.android.material.R.attr.colorPrimary + ) + ) + } + + override fun onDestroy() { + cameraManager.unregisterTorchCallback(torchListener) + super.onDestroy() + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + settingsMaterialButton.updateLayoutParams { + bottomMargin = insets.bottom + resources.getDimensionPixelSize(R.dimen.sprt_btn_marginBottom) + } + + flashlightSlider.updateLayoutParams { + topMargin = insets.top + } + + WindowInsetsCompat.CONSUMED + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/LevelFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/LevelFragment.kt new file mode 100644 index 0000000..4da4577 --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/fragments/LevelFragment.kt @@ -0,0 +1,160 @@ +package uk.akane.omni.ui.fragments + +import android.hardware.Sensor +import android.hardware.SensorEvent +import android.hardware.SensorEventListener +import android.hardware.SensorManager +import android.os.Bundle +import android.view.LayoutInflater +import android.view.Surface +import android.view.View +import android.view.ViewGroup +import androidx.core.content.ContextCompat +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import uk.akane.omni.R +import uk.akane.omni.logic.checkSensorAvailability +import uk.akane.omni.ui.MainActivity +import uk.akane.omni.ui.components.SpiritLevelView +import uk.akane.omni.ui.components.SwitchBottomSheet +import uk.akane.omni.ui.fragments.settings.MainSettingsFragment +import com.google.android.material.button.MaterialButton +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import kotlin.math.asin +import kotlin.math.sqrt + + +class LevelFragment : BaseFragment(), SensorEventListener { + + private var mainActivity: MainActivity? = null + + private var sensorManager: SensorManager? = null + + private var rotationVectorSensor: Sensor? = null + + private lateinit var sheetMaterialButton: MaterialButton + private lateinit var settingsMaterialButton: MaterialButton + + private lateinit var levelView: SpiritLevelView + + private var doNotHaveSensor: Boolean = false + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mainActivity = requireActivity() as MainActivity + + sensorManager = ContextCompat.getSystemService(requireContext(), SensorManager::class.java) + + rotationVectorSensor = sensorManager!!.getDefaultSensor(Sensor.TYPE_ROTATION_VECTOR) + if (!sensorManager!!.checkSensorAvailability(Sensor.TYPE_ROTATION_VECTOR)) { + mainActivity!!.postComplete() + doNotHaveSensor = true + } else { + sensorManager!!.registerListener(this, rotationVectorSensor, SensorManager.SENSOR_DELAY_FASTEST) + } + + } + + override fun onDestroy() { + sensorManager!!.unregisterListener(this) + super.onDestroy() + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.fragment_spirit_level, container, false) + + sheetMaterialButton = rootView.findViewById(R.id.sheet_btn)!! + settingsMaterialButton = rootView.findViewById(R.id.settings_btn)!! + + levelView = rootView.findViewById(R.id.level_view)!! + + settingsMaterialButton.setOnClickListener { + mainActivity!!.startFragment(MainSettingsFragment()) + } + + sheetMaterialButton.setOnClickListener { + SwitchBottomSheet(SwitchBottomSheet.CallFragmentType.SPIRIT_LEVEL).show(parentFragmentManager, "switch_bottom_sheet") + } + + if (doNotHaveSensor) { + MaterialAlertDialogBuilder(requireContext()) + .setTitle(resources.getString(R.string.warning_dialog_title)) + .setMessage(resources.getString(R.string.warning_dialog_text)) + .setIcon(R.drawable.ic_warning) + .setPositiveButton(resources.getString(R.string.dismiss), null) + .show() + } + + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + settingsMaterialButton.updateLayoutParams { + bottomMargin = insets.bottom + resources.getDimensionPixelSize(R.dimen.sprt_btn_marginBottom) + } + + WindowInsetsCompat.CONSUMED + } + } + + override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) { + // Do nothing + } + + override fun onSensorChanged(event: SensorEvent) { + when (event.sensor.type) { + Sensor.TYPE_ROTATION_VECTOR -> updateCompass(event) + } + if (mainActivity?.isInflationStarted() == false) { + mainActivity!!.postComplete() + } + } + + private fun updateCompass(event: SensorEvent) { + val rotationVector = floatArrayOf(event.values[0], event.values[1], event.values[2]) + + val rotationMatrix = FloatArray(16) + SensorManager.getRotationMatrixFromVector(rotationMatrix, rotationVector) + + val displayRotation = ContextCompat.getDisplayOrDefault(requireContext()).rotation + val remappedRotationMatrix = remapRotationMatrix(rotationMatrix, displayRotation) + + val orientationInRadians = FloatArray(3) + SensorManager.getOrientation(remappedRotationMatrix, orientationInRadians) + + val pitchInRadians = Math.toDegrees(orientationInRadians[1].toDouble()) + val rollInRadians = Math.toDegrees(orientationInRadians[2].toDouble()) + var balanceFactor: Float = sqrt( + remappedRotationMatrix[8] * remappedRotationMatrix[8] + + remappedRotationMatrix[9] * remappedRotationMatrix[9] + ) + balanceFactor = (if (balanceFactor == 0f) 0f else remappedRotationMatrix[8] / balanceFactor) + + val balance = Math.toDegrees(asin(balanceFactor).toDouble()).toFloat() + + levelView.updatePitchAndRollAndBalance(pitchInRadians.toFloat(), rollInRadians.toFloat(), balance) + } + + private fun remapRotationMatrix(rotationMatrix: FloatArray, displayRotation: Int?): FloatArray { + val (newX, newY) = when (displayRotation) { + Surface.ROTATION_90 -> Pair(SensorManager.AXIS_Y, SensorManager.AXIS_MINUS_X) + Surface.ROTATION_180 -> Pair(SensorManager.AXIS_MINUS_X, SensorManager.AXIS_MINUS_Y) + Surface.ROTATION_270 -> Pair(SensorManager.AXIS_MINUS_Y, SensorManager.AXIS_X) + else -> Pair(SensorManager.AXIS_X, SensorManager.AXIS_Y) + } + + val remappedRotationMatrix = FloatArray(16) + SensorManager.remapCoordinateSystem(rotationMatrix, newX, newY, remappedRotationMatrix) + return remappedRotationMatrix + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/RulerFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/RulerFragment.kt new file mode 100644 index 0000000..0a7befd --- /dev/null +++ b/app/src/main/java/uk/akane/omni/ui/fragments/RulerFragment.kt @@ -0,0 +1,68 @@ +package uk.akane.omni.ui.fragments + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.core.view.ViewCompat +import androidx.core.view.WindowInsetsCompat +import androidx.core.view.updateLayoutParams +import uk.akane.omni.R +import uk.akane.omni.ui.MainActivity +import uk.akane.omni.ui.components.SwitchBottomSheet +import uk.akane.omni.ui.fragments.settings.MainSettingsFragment +import com.google.android.material.button.MaterialButton + +class RulerFragment : BaseFragment() { + + private lateinit var sheetMaterialButton: MaterialButton + private lateinit var settingsMaterialButton: MaterialButton + private lateinit var scaleTopView: View + + private var mainActivity: MainActivity? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + mainActivity = requireActivity() as MainActivity + + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View? { + val rootView = inflater.inflate(R.layout.fragment_ruler, container, false) + + sheetMaterialButton = rootView.findViewById(R.id.sheet_btn)!! + settingsMaterialButton = rootView.findViewById(R.id.settings_btn)!! + scaleTopView = rootView.findViewById(R.id.card_layout_2)!! + + settingsMaterialButton.setOnClickListener { + mainActivity!!.startFragment(MainSettingsFragment()) + } + + sheetMaterialButton.setOnClickListener { + SwitchBottomSheet(SwitchBottomSheet.CallFragmentType.RULER).show(parentFragmentManager, "switch_bottom_sheet") + } + return rootView + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + ViewCompat.setOnApplyWindowInsetsListener(view) { _, windowInsets -> + val insets = windowInsets.getInsets(WindowInsetsCompat.Type.systemBars()) + + settingsMaterialButton.updateLayoutParams { + bottomMargin = insets.bottom + resources.getDimensionPixelSize(R.dimen.sprt_btn_marginBottom) + } + + scaleTopView.updateLayoutParams { + topMargin = insets.top + resources.getDimensionPixelSize(R.dimen.ruler_cv_up) + } + + WindowInsetsCompat.CONSUMED + } + } + +} \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/fragments/settings/MainSettingsFragment.kt b/app/src/main/java/uk/akane/omni/ui/fragments/settings/MainSettingsFragment.kt index b7815cb..06485f3 100644 --- a/app/src/main/java/uk/akane/omni/ui/fragments/settings/MainSettingsFragment.kt +++ b/app/src/main/java/uk/akane/omni/ui/fragments/settings/MainSettingsFragment.kt @@ -1,29 +1,10 @@ -/* - * Copyright (C) 2024 Akane Foundation - * - * Gramophone 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 3 of the License, or - * (at your option) any later version. - * - * Gramophone 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 this program. If not, see . - */ - package uk.akane.omni.ui.fragments.settings import android.os.Bundle import androidx.preference.Preference -import uk.akane.omni.BuildConfig import uk.akane.omni.R import uk.akane.omni.ui.fragments.BasePreferenceFragment import uk.akane.omni.ui.fragments.BaseSettingFragment -import uk.akane.omni.ui.fragments.CompassFragment class MainSettingsFragment : BaseSettingFragment( R.string.settings, @@ -33,7 +14,7 @@ class MainSettingsTopFragment : BasePreferenceFragment() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { setPreferencesFromResource(R.xml.settings_top, rootKey) val versionPrefs = findPreference("version") - versionPrefs!!.summary = BuildConfig.VERSION_NAME + versionPrefs!!.summary = requireContext().packageManager.getPackageInfo(requireContext().packageName, 0).versionName } } \ No newline at end of file diff --git a/app/src/main/java/uk/akane/omni/ui/viewmodels/OmniViewModel.kt b/app/src/main/java/uk/akane/omni/ui/viewmodels/OmniViewModel.kt index 22e68e1..df252ce 100644 --- a/app/src/main/java/uk/akane/omni/ui/viewmodels/OmniViewModel.kt +++ b/app/src/main/java/uk/akane/omni/ui/viewmodels/OmniViewModel.kt @@ -7,9 +7,15 @@ import androidx.lifecycle.ViewModel class OmniViewModel : ViewModel() { private val _lastKnownLocation = MutableLiveData() + private val _lastDegree = MutableLiveData() val lastKnownLocation: LiveData get() = _lastKnownLocation + val lastDegree: LiveData get() = _lastDegree fun setLastKnownLocation(location: Location) { _lastKnownLocation.value = location } + + fun setLastDegree(degree: Float) { + _lastDegree.value = degree + } } diff --git a/app/src/main/res/color/mtrl_btn_bg.xml b/app/src/main/res/color/mtrl_btn_bg.xml new file mode 100644 index 0000000..6ce362e --- /dev/null +++ b/app/src/main/res/color/mtrl_btn_bg.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/color/mtrl_btn_icon.xml b/app/src/main/res/color/mtrl_btn_icon.xml new file mode 100644 index 0000000..dcfff5c --- /dev/null +++ b/app/src/main/res/color/mtrl_btn_icon.xml @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_barometer.xml b/app/src/main/res/drawable/ic_barometer.xml new file mode 100644 index 0000000..53411d9 --- /dev/null +++ b/app/src/main/res/drawable/ic_barometer.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_coordinate_format.xml b/app/src/main/res/drawable/ic_coordinate_format.xml new file mode 100644 index 0000000..1af87f4 --- /dev/null +++ b/app/src/main/res/drawable/ic_coordinate_format.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_directions.xml b/app/src/main/res/drawable/ic_directions.xml new file mode 100644 index 0000000..26823fe --- /dev/null +++ b/app/src/main/res/drawable/ic_directions.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_explorer.xml b/app/src/main/res/drawable/ic_explorer.xml index e617c01..65d3cf9 100644 --- a/app/src/main/res/drawable/ic_explorer.xml +++ b/app/src/main/res/drawable/ic_explorer.xml @@ -5,6 +5,6 @@ android:viewportHeight="960" android:tint="?attr/colorControlNormal"> diff --git a/app/src/main/res/drawable/ic_flashlight_on.xml b/app/src/main/res/drawable/ic_flashlight_on.xml new file mode 100644 index 0000000..59882fb --- /dev/null +++ b/app/src/main/res/drawable/ic_flashlight_on.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications.xml b/app/src/main/res/drawable/ic_notifications.xml new file mode 100644 index 0000000..7746192 --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_omni.xml b/app/src/main/res/drawable/ic_omni.xml new file mode 100644 index 0000000..b3cb070 --- /dev/null +++ b/app/src/main/res/drawable/ic_omni.xml @@ -0,0 +1,17 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_pointer.xml b/app/src/main/res/drawable/ic_pointer.xml new file mode 100644 index 0000000..a8a9e72 --- /dev/null +++ b/app/src/main/res/drawable/ic_pointer.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_polygon_left.xml b/app/src/main/res/drawable/ic_polygon_left.xml new file mode 100644 index 0000000..0ccd9ff --- /dev/null +++ b/app/src/main/res/drawable/ic_polygon_left.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_polygon_right.xml b/app/src/main/res/drawable/ic_polygon_right.xml new file mode 100644 index 0000000..f081c23 --- /dev/null +++ b/app/src/main/res/drawable/ic_polygon_right.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_ruler.xml b/app/src/main/res/drawable/ic_ruler.xml new file mode 100644 index 0000000..fa23986 --- /dev/null +++ b/app/src/main/res/drawable/ic_ruler.xml @@ -0,0 +1,10 @@ + + + diff --git a/app/src/main/res/drawable/ic_straighten.xml b/app/src/main/res/drawable/ic_straighten.xml new file mode 100644 index 0000000..31e3ea8 --- /dev/null +++ b/app/src/main/res/drawable/ic_straighten.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_true_north.xml b/app/src/main/res/drawable/ic_true_north.xml new file mode 100644 index 0000000..9609e2d --- /dev/null +++ b/app/src/main/res/drawable/ic_true_north.xml @@ -0,0 +1,12 @@ + + + + diff --git a/app/src/main/res/drawable/ic_vibration.xml b/app/src/main/res/drawable/ic_vibration.xml new file mode 100644 index 0000000..0e0bb1c --- /dev/null +++ b/app/src/main/res/drawable/ic_vibration.xml @@ -0,0 +1,13 @@ + + + + + + diff --git a/app/src/main/res/drawable/ruler_left.xml b/app/src/main/res/drawable/ruler_left.xml new file mode 100644 index 0000000..ade8885 --- /dev/null +++ b/app/src/main/res/drawable/ruler_left.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/ruler_right.xml b/app/src/main/res/drawable/ruler_right.xml new file mode 100644 index 0000000..2839245 --- /dev/null +++ b/app/src/main/res/drawable/ruler_right.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_pref_down.xml b/app/src/main/res/drawable/shape_pref_down.xml new file mode 100644 index 0000000..741fb99 --- /dev/null +++ b/app/src/main/res/drawable/shape_pref_down.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_pref_mid.xml b/app/src/main/res/drawable/shape_pref_mid.xml new file mode 100644 index 0000000..969343b --- /dev/null +++ b/app/src/main/res/drawable/shape_pref_mid.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_pref_single.xml b/app/src/main/res/drawable/shape_pref_single.xml new file mode 100644 index 0000000..2caba0f --- /dev/null +++ b/app/src/main/res/drawable/shape_pref_single.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/shape_pref_up.xml b/app/src/main/res/drawable/shape_pref_up.xml new file mode 100644 index 0000000..c31577a --- /dev/null +++ b/app/src/main/res/drawable/shape_pref_up.xml @@ -0,0 +1,19 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/hankengrotesk.ttf b/app/src/main/res/font/hankengrotesk.ttf deleted file mode 100644 index 0b92c22..0000000 Binary files a/app/src/main/res/font/hankengrotesk.ttf and /dev/null differ diff --git a/app/src/main/res/font/hankengrotesk.xml b/app/src/main/res/font/hankengrotesk.xml new file mode 100644 index 0000000..5012359 --- /dev/null +++ b/app/src/main/res/font/hankengrotesk.xml @@ -0,0 +1,11 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/font/hg.ttf b/app/src/main/res/font/hg.ttf new file mode 100644 index 0000000..8b01d15 Binary files /dev/null and b/app/src/main/res/font/hg.ttf differ diff --git a/app/src/main/res/font/hgm.ttf b/app/src/main/res/font/hgm.ttf new file mode 100644 index 0000000..81d89ef Binary files /dev/null and b/app/src/main/res/font/hgm.ttf differ diff --git a/app/src/main/res/layout-land/fragment_compass.xml b/app/src/main/res/layout-land/fragment_compass.xml index d8b4dec..207620a 100644 --- a/app/src/main/res/layout-land/fragment_compass.xml +++ b/app/src/main/res/layout-land/fragment_compass.xml @@ -76,7 +76,6 @@ android:insetRight="0dp" android:insetTop="0dp" android:layout_marginBottom="8dp" - android:visibility="gone" app:layout_constraintStart_toStartOf="@id/settings_btn" app:layout_constraintBottom_toTopOf="@id/settings_btn" app:iconGravity="textStart" @@ -116,7 +115,7 @@ android:text="@string/longitude_desc" android:textSize="16sp" android:visibility="gone" - android:textFontWeight="600" + android:textFontWeight="500" android:textColor="?colorOutline" android:includeFontPadding="false" android:layout_marginTop="5dp" @@ -134,7 +133,7 @@ android:textSize="16sp" android:layout_marginBottom="5dp" android:textColor="?colorOutline" - android:textFontWeight="600" + android:textFontWeight="500" android:visibility="gone" android:includeFontPadding="false" app:layout_constraintTop_toBottomOf="@id/latitude_desc" @@ -149,7 +148,7 @@ tools:text="40.7493°" android:text="@string/position_default_text" android:includeFontPadding="false" - android:textFontWeight="600" + android:textFontWeight="500" android:textColor="?colorOnSurface" android:visibility="gone" android:textSize="16sp" @@ -166,7 +165,7 @@ tools:text="73.9679°" android:text="@string/position_default_text" android:includeFontPadding="false" - android:textFontWeight="600" + android:textFontWeight="500" android:textColor="?colorOnSurface" android:textSize="16sp" android:visibility="gone" diff --git a/app/src/main/res/layout/fragment_compass.xml b/app/src/main/res/layout/fragment_compass.xml index 97c4491..7398ae6 100644 --- a/app/src/main/res/layout/fragment_compass.xml +++ b/app/src/main/res/layout/fragment_compass.xml @@ -1,5 +1,6 @@ - + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_ruler.xml b/app/src/main/res/layout/fragment_ruler.xml new file mode 100644 index 0000000..37cc3ee --- /dev/null +++ b/app/src/main/res/layout/fragment_ruler.xml @@ -0,0 +1,173 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_spirit_level.xml b/app/src/main/res/layout/fragment_spirit_level.xml new file mode 100644 index 0000000..4c320d8 --- /dev/null +++ b/app/src/main/res/layout/fragment_spirit_level.xml @@ -0,0 +1,63 @@ + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/fragment_top_settings.xml b/app/src/main/res/layout/fragment_top_settings.xml index bb8804e..fecf3c7 100644 --- a/app/src/main/res/layout/fragment_top_settings.xml +++ b/app/src/main/res/layout/fragment_top_settings.xml @@ -1,56 +1,29 @@ - - - + + android:layout_height="match_parent" + android:background="?colorSurfaceContainer"> + android:stateListAnimator="@null" + app:liftOnScroll="false" + app:liftOnScrollColor="?colorSurfaceContainer" + android:background="?colorSurfaceContainer"> - - - - - + android:layout_height="?attr/actionBarSize" + android:elevation="0dp" + app:layout_collapseMode="pin" + app:navigationIcon="@drawable/ic_arrow_back" + android:background="?colorSurfaceContainer" + app:navigationIconTint="?colorOnSurface" /> diff --git a/app/src/main/res/layout/preference_basic_down.xml b/app/src/main/res/layout/preference_basic_down.xml new file mode 100644 index 0000000..d00e8af --- /dev/null +++ b/app/src/main/res/layout/preference_basic_down.xml @@ -0,0 +1,67 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_basic_mid.xml b/app/src/main/res/layout/preference_basic_mid.xml new file mode 100644 index 0000000..97d8a53 --- /dev/null +++ b/app/src/main/res/layout/preference_basic_mid.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_basic_single.xml b/app/src/main/res/layout/preference_basic_single.xml new file mode 100644 index 0000000..179e58f --- /dev/null +++ b/app/src/main/res/layout/preference_basic_single.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_basic_top.xml b/app/src/main/res/layout/preference_basic_top.xml new file mode 100644 index 0000000..7f60625 --- /dev/null +++ b/app/src/main/res/layout/preference_basic_top.xml @@ -0,0 +1,66 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_cate.xml b/app/src/main/res/layout/preference_cate.xml new file mode 100644 index 0000000..d3e4951 --- /dev/null +++ b/app/src/main/res/layout/preference_cate.xml @@ -0,0 +1,22 @@ + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/preference_switch_widget.xml b/app/src/main/res/layout/preference_switch_widget.xml new file mode 100644 index 0000000..94d654f --- /dev/null +++ b/app/src/main/res/layout/preference_switch_widget.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/switch_bottom_sheet.xml b/app/src/main/res/layout/switch_bottom_sheet.xml new file mode 100644 index 0000000..6aac6c4 --- /dev/null +++ b/app/src/main/res/layout/switch_bottom_sheet.xml @@ -0,0 +1,121 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values-zh-rCN/strings.xml b/app/src/main/res/values-zh-rCN/strings.xml index 5c6a6c2..f21ac44 100644 --- a/app/src/main/res/values-zh-rCN/strings.xml +++ b/app/src/main/res/values-zh-rCN/strings.xml @@ -13,7 +13,7 @@ 东经 未知地点 位置 - Omni 需要用到您的位置信息来展示经纬度。这可能需要与您的位置提供商进行额外通讯。如果您选择“不”,您仍然可以使用 Omni 指南针 的基础功能。 + 工具箱需要用到您的位置信息来展示经纬度。这可能需要与您的位置提供商进行额外通讯。如果您选择“不”,您仍然可以使用工具箱指南针 的基础功能。 不要 好的 设置 @@ -22,8 +22,34 @@ 版本 未知版本 警告 - 看起来您的设备没有可用的旋转向量传感器。 Omni 的指南针功能将被禁用。 但是,您仍然可以使用位置功能。 + 看起来您的设备没有可用的旋转向量传感器。工具箱的指南针功能将被禁用。 但是,您仍然可以使用位置功能。 关闭 "" %9$s%7$s%8$s%5$s%6$s%3$s%4$s%1$s%2$s + 不支持 + 指南针 + 指南针通知 + 指南针正在运行 + 长按来关闭这个通知 + 关于 + 坐标格式 + 度,分,秒 (DMS) + 指南针方向 + 次级方位 + 使用真北 + 通用 + 震动反馈 + 高亮度可能造成灼伤。 + 通知 + 工具箱需要通知权限来让快速设置里的指南针磁铁保持存活。如果你选择\"不要\",指南针磁铁可能无法正常工作。 + 指南针 + 指南针 + 水平仪 + 水平仪 + 尺子 + 尺子 + 手电筒 + 手电筒 + 警告 + 将手电筒亮度调至 90% 及更高可能会对手电筒的发光元件造成不可逆转的损毁。如果还是要继续,请按 “好的”。 \ No newline at end of file diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml index 54538fe..1bf4c02 100644 --- a/app/src/main/res/values/dimens.xml +++ b/app/src/main/res/values/dimens.xml @@ -1,4 +1,5 @@ 16dp + 32dp \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0c0b359..2e93463 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -21,13 +21,14 @@ 300 330 " %1$s°" + "%1$s°" N E Unknown location Location No Yes - Omni requires your location data in order to display latitude and longitude. That may requires communications with your location provider\'s server. If you select \"no\", you still can use the basic functionalities of omni compass. + Toolbox requires your location data in order to display latitude and longitude. That may requires communications with your location provider\'s server. If you select \"no\", you still can use the basic functionalities of compass. __.____° Settings App name @@ -36,8 +37,37 @@ Version Unknown version Warning - It looks like your device doesn\'t have an available rotation vector sensor. The compass feature of Omni will be disabled. However, you still can use the location feature. + It looks like your device doesn\'t have an available rotation vector sensor. The compass feature of toolbox will be disabled. However, you still can use the location feature. Dismiss ,\ %1$s%2$s%3$s%4$s%5$s%6$s%7$s%8$s%9$s + Not supported! + Compass + Compass Notification + Compass running + Long click to disable this notification + About + Coordinate format + Degree, minute, second (DMS) + Compass directions + Intercardinal directions + Use true north + Universal + Haptic feedback + cm + in + Higher brightness may cause burns + %1$s° %2$s\' %3$s\'\' + Notification + Toolbox requires notification permission in order to post notification for the compass QS tile to survive. If you select no, compass QS tile may not work as expected. + Compass + Compass + Spirit level + Spirit level + Ruler + Ruler + Flashlight + Flashlight + Warning + Increasing the flashlight brightness to 90% or higher may cause irreversible damage to the flashlight\'s light-emitting components. If you still want to proceed, press "Yes". \ No newline at end of file diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 1a48fc1..446e10d 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -6,6 +6,7 @@ @font/hankengrotesk @font/hankengrotesk uk.akane.omni.logic.ui.ViewCompatInflater + @style/ThemeOverlay.App.BottomSheetDialog none @style/ThemeOverlay.App.MaterialAlertDialog @@ -86,7 +87,7 @@ + +