diff --git a/app/build.gradle b/app/build.gradle index 1a65c84b..6ee16331 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -66,11 +66,12 @@ dependencies { implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.3.9" //implementation "org.jetbrains.kotlinx:kotlinx-serialization-core:1.0.0-RC" implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'androidx.constraintlayout:constraintlayout:2.0.1' + implementation "androidx.activity:activity-ktx:1.2.0-alpha08" + implementation 'androidx.fragment:fragment-ktx:1.3.0-alpha08' implementation 'androidx.preference:preference:1.1.1' + implementation 'androidx.constraintlayout:constraintlayout:2.0.1' implementation 'androidx.gridlayout:gridlayout:1.0.0' implementation "androidx.biometric:biometric:1.0.1" - implementation 'androidx.fragment:fragment-ktx:1.2.5' implementation "com.daimajia.swipelayout:library:1.2.0@aar" implementation 'com.google.android.material:material:1.3.0-alpha02' implementation 'com.google.firebase:firebase-core:17.5.0' diff --git a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt index 4710f246..c5cc4233 100644 --- a/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt +++ b/app/src/main/java/xyz/quaver/pupil/ui/ReaderActivity.kt @@ -18,11 +18,14 @@ package xyz.quaver.pupil.ui +import android.Manifest import android.content.ComponentName import android.content.Intent import android.content.ServiceConnection +import android.content.pm.PackageManager import android.graphics.drawable.Animatable import android.graphics.drawable.Drawable +import android.os.Build import android.os.Bundle import android.os.IBinder import android.view.* @@ -31,9 +34,9 @@ import android.view.animation.AnticipateInterpolator import android.view.animation.OvershootInterpolator import android.view.animation.TranslateAnimation import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AlertDialog import androidx.core.content.ContextCompat -import androidx.core.content.res.ResourcesCompat import androidx.preference.PreferenceManager import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.PagerSnapHelper @@ -43,6 +46,7 @@ import androidx.vectordrawable.graphics.drawable.AnimatedVectorDrawableCompat import com.bumptech.glide.Glide import com.google.android.material.snackbar.Snackbar import com.google.firebase.crashlytics.FirebaseCrashlytics +import com.google.mlkit.vision.face.Face import com.qtalk.recyclerviewfastscroller.RecyclerViewFastScroller import kotlinx.android.synthetic.main.activity_reader.* import kotlinx.android.synthetic.main.activity_reader.view.* @@ -50,6 +54,7 @@ import kotlinx.android.synthetic.main.dialog_numberpicker.view.* import kotlinx.android.synthetic.main.reader_eye_card.view.* import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.delay import kotlinx.coroutines.launch import xyz.quaver.Code import xyz.quaver.pupil.R @@ -62,7 +67,7 @@ import xyz.quaver.pupil.util.camera import xyz.quaver.pupil.util.closeCamera import xyz.quaver.pupil.util.downloader.Cache import xyz.quaver.pupil.util.downloader.DownloadManager -import xyz.quaver.pupil.util.testCamera +import xyz.quaver.pupil.util.startCamera import java.util.* import kotlin.concurrent.schedule @@ -97,11 +102,29 @@ class ReaderActivity : BaseActivity() { } private val timer = Timer() - private val snapHelper = PagerSnapHelper() - private var menu: Menu? = null + private val requestPermissionLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { isGranted -> + if (isGranted) + toggleCamera() + else + AlertDialog.Builder(this) + .setTitle(R.string.error) + .setMessage(R.string.camera_denied) + .setPositiveButton(android.R.string.ok) { _, _ ->} + .show() + } + + enum class Eye { + LEFT, + RIGHT + } + + private var cameraEnabled = false + private var eyeType: Eye? = null + private var eyeCount: Int = 0 + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_reader) @@ -226,6 +249,18 @@ class ReaderActivity : BaseActivity() { return true } + override fun onResume() { + super.onResume() + + if (cameraEnabled) + startCamera(this, cameraCallback) + } + + override fun onPause() { + super.onPause() + closeCamera() + } + override fun onDestroy() { super.onDestroy() @@ -390,66 +425,21 @@ class ReaderActivity : BaseActivity() { } with(reader_fab_auto) { - setImageResource(R.drawable.clock_start) + setImageResource(R.drawable.eye_white) setOnClickListener { - val eyes = this@ReaderActivity.eye_card - when (camera) { - null -> { - eyes.apply { - visibility = View.VISIBLE - TranslateAnimation(0F, 0F, -100F, 0F).apply { - duration = 500 - fillAfter = false - interpolator = OvershootInterpolator() - }.let { startAnimation(it) } - } - testCamera(context) { faces -> - eyes.dot.let { - it.visibility = View.VISIBLE - Timer().schedule(50) { - runOnUiThread { - it.visibility = View.GONE - } - } - } - - if (faces.size != 1) - ResourcesCompat.getDrawable(resources, R.drawable.eye_off, context.theme).let { - eyes.left_eye.setImageDrawable(it) - eyes.right_eye.setImageDrawable(it) - - return@testCamera - } - - val left = ResourcesCompat.getDrawable(resources, - if (faces[0].rightEyeOpenProbability?.let { it > 0.4 } == true) R.drawable.eye else R.drawable.eye_closed, - context.theme) - val right = ResourcesCompat.getDrawable(resources, - if (faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true) R.drawable.eye else R.drawable.eye_closed, - context.theme) - - eyes.left_eye.setImageDrawable(left) - eyes.right_eye.setImageDrawable(right) - } + when { + ContextCompat.checkSelfPermission(context, Manifest.permission.CAMERA) == PackageManager.PERMISSION_GRANTED -> { + toggleCamera() } - else -> { - eyes.apply { - TranslateAnimation(0F, 0F, 0F, -100F).apply { - duration = 500 - fillAfter = false - interpolator = AnticipateInterpolator() - setAnimationListener(object: Animation.AnimationListener { - override fun onAnimationStart(p0: Animation?) {} - override fun onAnimationRepeat(p0: Animation?) {} - - override fun onAnimationEnd(p0: Animation?) { - eyes.visibility = View.GONE - } - }) - }.let { startAnimation(it) } - } - closeCamera() + Build.VERSION.SDK_INT >= 23 && shouldShowRequestPermissionRationale(Manifest.permission.CAMERA) -> { + AlertDialog.Builder(this@ReaderActivity) + .setTitle(R.string.warning) + .setMessage(R.string.camera_denied) + .setPositiveButton(android.R.string.ok) { _, _ ->} + .show() } + else -> + requestPermissionLauncher.launch(Manifest.permission.CAMERA) } } } @@ -502,6 +492,17 @@ class ReaderActivity : BaseActivity() { } else { snapHelper.attachToRecyclerView(reader_recyclerview) reader_recyclerview.layoutManager = LinearLayoutManager(this, LinearLayoutManager.HORIZONTAL, Preferences["rtl", false]) + + if (Preferences["rtl", false]) + with(reader_progressbar) { + scaleX = -1F + translationX = 1F + } + else + with(reader_progressbar) { + scaleX = 0F + translationX = 0F + } } (reader_recyclerview.layoutManager as LinearLayoutManager).scrollToPositionWithOffset(currentPage-1, 0) @@ -536,6 +537,125 @@ class ReaderActivity : BaseActivity() { } } + val cameraCallback: (List) -> Unit = callback@{ faces -> + eye_card.dot.let { + it.visibility = View.VISIBLE + CoroutineScope(Dispatchers.Main).launch { + delay(50) + it.visibility = View.INVISIBLE + } + } + + if (faces.size != 1) + ContextCompat.getDrawable(this, R.drawable.eye_off).let { + with(eye_card) { + left_eye.setImageDrawable(it) + right_eye.setImageDrawable(it) + } + + return@callback + } + + val (left, right) = Pair( + faces[0].rightEyeOpenProbability?.let { it > 0.4 } == true, + faces[0].leftEyeOpenProbability?.let { it > 0.4 } == true + ) + + with(eye_card) { + left_eye.setImageDrawable( + ContextCompat.getDrawable( + context, + if (left) R.drawable.eye else R.drawable.eye_closed + ) + ) + right_eye.setImageDrawable( + ContextCompat.getDrawable( + context, + if (right) R.drawable.eye else R.drawable.eye_closed + ) + ) + } + + when { + // Both closed / opened + !left.xor(right) -> { + eyeType = null + eyeCount = 0 + } + !left -> { + if (eyeType != Eye.LEFT) { + eyeType = Eye.LEFT + eyeCount = 0 + } + eyeCount++ + } + !right -> { + if (eyeType != Eye.RIGHT) { + eyeType = Eye.RIGHT + eyeCount = 0 + } + eyeCount++ + } + } + + if (eyeCount > 3) { + (this@ReaderActivity.reader_recyclerview.layoutManager as LinearLayoutManager).let { + it.scrollToPositionWithOffset(when(eyeType!!) { + Eye.RIGHT -> { + if (it.reverseLayout) currentPage - 2 else currentPage + } + Eye.LEFT -> { + if (it.reverseLayout) currentPage else currentPage - 2 + } + }, 0) + } + + eyeType = null + eyeCount = 0 + } + } + + private fun toggleCamera() { + val eyes = this@ReaderActivity.eye_card + when (camera) { + null -> { + reader_fab_auto.labelText = getString(R.string.reader_fab_auto_cancel) + reader_fab_auto.setImageResource(R.drawable.eye_off_white) + eyes.apply { + visibility = View.VISIBLE + TranslateAnimation(0F, 0F, -100F, 0F).apply { + duration = 500 + fillAfter = false + interpolator = OvershootInterpolator() + }.let { startAnimation(it) } + } + startCamera(this, cameraCallback) + cameraEnabled = true + } + else -> { + reader_fab_auto.labelText = getString(R.string.reader_fab_auto) + reader_fab_auto.setImageResource(R.drawable.eye_white) + eyes.apply { + TranslateAnimation(0F, 0F, 0F, -100F).apply { + duration = 500 + fillAfter = false + interpolator = AnticipateInterpolator() + setAnimationListener(object: Animation.AnimationListener { + override fun onAnimationStart(p0: Animation?) {} + override fun onAnimationRepeat(p0: Animation?) {} + + override fun onAnimationEnd(p0: Animation?) { + eyes.visibility = View.GONE + } + }) + }.let { startAnimation(it) } + } + closeCamera() + cameraEnabled = false + } + } + } + override fun onLowMemory() { super.onLowMemory() Glide.get(this).onLowMemory() diff --git a/app/src/main/java/xyz/quaver/pupil/util/camera.kt b/app/src/main/java/xyz/quaver/pupil/util/camera.kt index f783a9e4..1b2e5652 100644 --- a/app/src/main/java/xyz/quaver/pupil/util/camera.kt +++ b/app/src/main/java/xyz/quaver/pupil/util/camera.kt @@ -16,13 +16,14 @@ * along with this program. If not, see . */ -@file:Suppress("DEPRECATION") +@file:Suppress("DEPRECATION", "Recycle") package xyz.quaver.pupil.util import android.content.Context import android.content.pm.PackageManager import android.graphics.ImageFormat +import android.graphics.SurfaceTexture import android.hardware.Camera import android.view.Surface import android.view.WindowManager @@ -68,6 +69,7 @@ private fun getRotation(context: Context, cameraID: Int): Int { } var camera: Camera? = null +var surfaceTexture: SurfaceTexture? = null private val detector = FaceDetection.getClient( FaceDetectorOptions.Builder() .setClassificationMode(FaceDetectorOptions.CLASSIFICATION_MODE_ALL) @@ -75,7 +77,7 @@ private val detector = FaceDetection.getClient( ) private var process: Task>? = null -fun testCamera(context: Context, callback: (List) -> Unit) { +fun startCamera(context: Context, callback: (List) -> Unit) { if (camera != null) closeCamera() val cameraID = openFrontCamera().let { (cam, cameraID) -> @@ -88,9 +90,13 @@ fun testCamera(context: Context, callback: (List) -> Unit) { parameters = parameters.apply { setPreviewSize(640, 480) previewFormat = ImageFormat.NV21 - flashMode = Camera.Parameters.FLASH_MODE_OFF } - setPreviewCallback { bytes, camera -> + + setPreviewTexture(surfaceTexture ?: SurfaceTexture(0).also { + surfaceTexture = it + }) + startPreview() + setPreviewCallback { bytes, _ -> if (process?.isComplete == false) return@setPreviewCallback @@ -100,14 +106,14 @@ fun testCamera(context: Context, callback: (List) -> Unit) { process = detector.process(image) .addOnSuccessListener(callback) } - - startPreview() } } fun closeCamera() { camera?.setPreviewCallback(null) camera?.stopPreview() + surfaceTexture?.release() + surfaceTexture = null camera?.release() camera = null } \ No newline at end of file diff --git a/app/src/main/res/drawable/eye_off_white.xml b/app/src/main/res/drawable/eye_off_white.xml new file mode 100644 index 00000000..15afd4cd --- /dev/null +++ b/app/src/main/res/drawable/eye_off_white.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/eye_white.xml b/app/src/main/res/drawable/eye_white.xml new file mode 100644 index 00000000..c1f3d205 --- /dev/null +++ b/app/src/main/res/drawable/eye_white.xml @@ -0,0 +1,8 @@ + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/activity_reader.xml b/app/src/main/res/layout/activity_reader.xml index cfa8c1f1..0c19d226 100644 --- a/app/src/main/res/layout/activity_reader.xml +++ b/app/src/main/res/layout/activity_reader.xml @@ -106,7 +106,7 @@ android:id="@+id/reader_fab_auto" android:layout_width="wrap_content" android:layout_height="wrap_content" - app:srcCompat="@drawable/clock_start" + app:srcCompat="@drawable/eye_white" app:fab_label="@string/reader_fab_auto" app:fab_size="mini"/> diff --git a/app/src/main/res/layout/item_mirrors.xml b/app/src/main/res/layout/item_mirrors.xml index 5c849c09..2b349771 100644 --- a/app/src/main/res/layout/item_mirrors.xml +++ b/app/src/main/res/layout/item_mirrors.xml @@ -42,7 +42,6 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" app:srcCompat="@drawable/menu" - app:tint="?attr/colorControlNormal" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintTop_toTopOf="parent" app:layout_constraintBottom_toBottomOf="parent" diff --git a/app/src/main/res/layout/reader_eye_card.xml b/app/src/main/res/layout/reader_eye_card.xml index b199a922..11343d53 100644 --- a/app/src/main/res/layout/reader_eye_card.xml +++ b/app/src/main/res/layout/reader_eye_card.xml @@ -55,7 +55,7 @@ android:id="@+id/dot" android:layout_width="4dp" android:layout_height="4dp" - android:visibility="gone" + android:visibility="invisible" app:srcCompat="@drawable/dot" app:layout_constraintLeft_toLeftOf="@id/left_eye" app:layout_constraintRight_toRightOf="@id/right_eye" diff --git a/app/src/main/res/values-ja/strings.xml b/app/src/main/res/values-ja/strings.xml index ffce239f..8fb154ec 100644 --- a/app/src/main/res/values-ja/strings.xml +++ b/app/src/main/res/values-ja/strings.xml @@ -132,7 +132,7 @@ ユーザーID ユーザーIDをクリップボードにコピーしました リトライ - 自動スクロール + まばたき検知スクロール 全てのギャラリーを対象に検索 綴じ方向を左にする ブックマーク管理 @@ -148,4 +148,8 @@ オープンソースライセンス お気に入りのタグを見る 履歴を見る + まばたき検知を中止 + カメラ権限が拒否されているため、まばたき検知使用できません + この機器には前面カメラが装着されていません + エラー \ No newline at end of file diff --git a/app/src/main/res/values-ko/strings.xml b/app/src/main/res/values-ko/strings.xml index f18284ed..74ad60e7 100644 --- a/app/src/main/res/values-ko/strings.xml +++ b/app/src/main/res/values-ko/strings.xml @@ -132,7 +132,7 @@ 유저 ID 유저 ID를 클립보드에 복사했습니다 재시도 - 자동 스크롤 + 눈 깜빡임 감지 스크롤 모든 갤러리 검색 좌측으로 페이지 넘기기 즐겨찾기 관리 @@ -148,4 +148,8 @@ 오픈 소스 라이선스 검색 기록 보기 즐겨찾기 태그 보기 + 눈 깜빡임 감지 중지 + 카메라 권한이 거부되었기 때문에 눈 깜빡임 감지가 불가능합니다 + 이 장치에는 전면 카메라가 없습니다 + 오류 \ 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 7695d3e4..b9c18a0a 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -20,6 +20,7 @@ Warning + Error Ignore @@ -99,12 +100,16 @@ Go to page Fullscreen> Retry - Automatic scroll + Scroll with eye blink + Stop scroll with eye blink Background download Cancel background download Downloading… Download complete + Eye blink detection cannot be used without a permission + There is no front facing camera in this device + Downloader running…