From 4346d33b59546105609598a14de6790f2c59e11c Mon Sep 17 00:00:00 2001 From: tateisu Date: Mon, 30 Dec 2019 03:55:33 +0900 Subject: [PATCH] =?UTF-8?q?=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9=E3=81=AE?= =?UTF-8?q?=E5=AE=9A=E7=BE=A9=E3=81=ABandroid:stopWithTask=3D"false"?= =?UTF-8?q?=E5=B1=9E=E6=80=A7=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82=20?= =?UTF-8?q?=E8=A8=AD=E5=AE=9A=E7=94=BB=E9=9D=A2=E3=81=AB"=E9=8C=B2?= =?UTF-8?q?=E7=94=BB=E5=81=9C=E6=AD=A2"=E3=83=9C=E3=82=BF=E3=83=B3?= =?UTF-8?q?=E3=82=92=E8=BF=BD=E5=8A=A0=E3=80=82=20=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=81=AB=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?= =?UTF-8?q?=E3=81=AE=E8=B5=B7=E5=8B=95=E7=8A=B6=E6=85=8B=E3=82=92=E8=A1=A8?= =?UTF-8?q?=E7=A4=BA=E3=81=99=E3=82=8B=E3=80=82=20=E8=A8=AD=E5=AE=9A?= =?UTF-8?q?=E7=94=BB=E9=9D=A2=E3=81=AB=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9?= =?UTF-8?q?=E3=81=8C=E5=81=9C=E6=AD=A2=E3=81=97=E3=81=9F=E7=90=86=E7=94=B1?= =?UTF-8?q?=E3=82=92=E8=A1=A8=E7=A4=BA=E3=81=99=E3=82=8B=E3=80=82=20onTask?= =?UTF-8?q?Removed=E7=99=BA=E7=94=9F=E6=99=82=E3=81=ABstopSelf=E3=82=92?= =?UTF-8?q?=E5=91=BC=E3=81=B0=E3=81=AA=E3=81=84=E3=80=82=E3=81=BE=E3=81=9F?= =?UTF-8?q?Alarm=E7=B5=8C=E7=94=B1=E3=81=A7=E5=B0=91=E3=81=97=E5=BE=8C?= =?UTF-8?q?=E3=81=AB=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9=E3=82=92=E9=96=8B?= =?UTF-8?q?=E5=A7=8B=E3=81=97=E3=81=AA=E3=81=8A=E3=81=99=E3=80=82=20?= =?UTF-8?q?=E4=B8=8A=E3=81=AB=E9=96=A2=E9=80=A3=E3=81=97=E3=81=A6=E3=80=81?= =?UTF-8?q?=E3=82=B5=E3=83=BC=E3=83=93=E3=82=B9=E9=96=8B=E5=A7=8B=E6=99=82?= =?UTF-8?q?=E3=81=AEIntent=E3=81=ABMediaProjection=E4=BD=9C=E6=88=90?= =?UTF-8?q?=E3=81=AB=E5=BF=85=E8=A6=81=E3=81=AA=E6=83=85=E5=A0=B1=E3=82=92?= =?UTF-8?q?=E4=BF=9D=E6=8C=81=E3=81=99=E3=82=8B=E3=80=82=20PendingIntent?= =?UTF-8?q?=E7=94=A8=E3=81=AErequestCode=E3=81=AE=E5=80=A4=E3=81=8C?= =?UTF-8?q?=E9=87=8D=E8=A4=87=E3=81=97=E3=81=A6=E3=81=84=E3=81=9F=E3=83=90?= =?UTF-8?q?=E3=82=B0=E3=82=92=E4=BF=AE=E6=AD=A3=E3=80=82=20getVolumePathAp?= =?UTF-8?q?i21()=20=E3=81=AE=E8=BB=BD=E5=BE=AE=E3=81=AA=E3=83=90=E3=82=B0?= =?UTF-8?q?=E3=82=92=E4=BF=AE=E6=AD=A3=E3=80=82=20=E7=94=BB=E9=9D=A2?= =?UTF-8?q?=E3=82=B5=E3=82=A4=E3=82=BA=E5=8F=96=E5=BE=97=E6=99=82=E3=81=AB?= =?UTF-8?q?WindowManager.defaultDisplay=E3=81=A7=E3=81=AF=E3=81=AA?= =?UTF-8?q?=E3=81=8FDisplayManager.getDisplay=E3=82=92=E4=BD=BF=E3=81=86?= =?UTF-8?q?=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/src/main/AndroidManifest.xml | 2 + .../jp/juggler/screenshotbutton/ActMain.kt | 110 ++++++++-- .../jp/juggler/screenshotbutton/Capture.kt | 18 +- .../screenshotbutton/CaptureServiceBase.kt | 198 ++++++++++++++---- .../screenshotbutton/CaptureServiceStill.kt | 17 +- .../screenshotbutton/CaptureServiceVideo.kt | 19 +- .../java/jp/juggler/screenshotbutton/IDs.kt | 7 +- .../jp/juggler/screenshotbutton/MyReceiver.kt | 24 +-- .../main/java/jp/juggler/util/LogCategory.kt | 4 +- .../main/java/jp/juggler/util/StorageUtils.kt | 81 ++++--- app/src/main/java/jp/juggler/util/Utils.kt | 107 +++++----- app/src/main/res/layout/act_main.xml | 148 +++++++------ app/src/main/res/values-ja/strings.xml | 11 +- app/src/main/res/values/strings.xml | 4 + 14 files changed, 481 insertions(+), 269 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 79cc405..112fe9f 100755 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -37,12 +37,14 @@ diff --git a/app/src/main/java/jp/juggler/screenshotbutton/ActMain.kt b/app/src/main/java/jp/juggler/screenshotbutton/ActMain.kt index 0499891..4cd312f 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/ActMain.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/ActMain.kt @@ -40,18 +40,21 @@ class ActMain : AppCompatActivity(), View.OnClickListener { private lateinit var btnStartStopStill: Button private lateinit var btnStartStopVideo: Button + private lateinit var btnStopRecording: Button private lateinit var tvButtonSizeError: TextView private lateinit var tvSaveFolder: TextView private lateinit var tvCodec: TextView + private lateinit var tvStatusStill: TextView + private lateinit var tvStatusVideo: TextView private var timeStartButtonTappedStill = 0L private var timeStartButtonTappedVideo = 0L override fun onCreate(savedInstanceState: Bundle?) { + App1.prepareAppState(this) log.d("onCreate savedInstanceState=$savedInstanceState") refActivity = WeakReference(this) super.onCreate(savedInstanceState) - App1.prepareAppState(this) initUI() } @@ -98,11 +101,14 @@ class ActMain : AppCompatActivity(), View.OnClickListener { openSaveTreeUriChooser() R.id.btnStartStopStill -> - if (CaptureServiceStill.isAlive()) { - stopService(Intent(this, CaptureServiceStill::class.java)) - } else { - timeStartButtonTappedStill = SystemClock.elapsedRealtime() - dispatch() + when (val service = CaptureServiceStill.getService()) { + null -> { + timeStartButtonTappedStill = SystemClock.elapsedRealtime() + dispatch() + } + else -> { + service.stopWithReason("StopButton") + } } R.id.btnResetPositionStill -> { @@ -114,17 +120,20 @@ class ActMain : AppCompatActivity(), View.OnClickListener { } R.id.btnStartStopVideo -> - if (CaptureServiceVideo.isAlive()) { - stopService(Intent(this, CaptureServiceVideo::class.java)) - } else { - try { - Capture.loadVideoSetting(this, App1.pref) - } catch (ex: Throwable) { - log.eToast(this, ex, "Video setting error.") - return + when (val service = CaptureServiceVideo.getService()) { + null -> { + try { + Capture.loadVideoSetting(this, App1.pref) + } catch (ex: Throwable) { + log.eToast(this, ex, "Video setting error.") + return + } + timeStartButtonTappedVideo = SystemClock.elapsedRealtime() + dispatch() + } + else -> { + service.stopWithReason("StopButton") } - timeStartButtonTappedVideo = SystemClock.elapsedRealtime() - dispatch() } R.id.btnResetPositionVideo -> { @@ -145,6 +154,12 @@ class ActMain : AppCompatActivity(), View.OnClickListener { } ad.show(this, getString(R.string.codec)) } + + R.id.btnStopRecording -> + CaptureServiceVideo.getService() + .runOnService(this) { + captureStop() + } } } @@ -160,15 +175,20 @@ class ActMain : AppCompatActivity(), View.OnClickListener { val screenWidth = dm.widthPixels val pageWidth = 360f.dp2px(dm) val remain = max(0, screenWidth - pageWidth) - (findViewById(R.id.svRoot).layoutParams as? ViewGroup.MarginLayoutParams) + + findViewById(R.id.svRoot) + .layoutParams + .castOrNull() ?.marginEnd = remain btnStartStopStill = findViewById(R.id.btnStartStopStill) btnStartStopVideo = findViewById(R.id.btnStartStopVideo) + btnStopRecording = findViewById(R.id.btnStopRecording) arrayOf( btnStartStopStill, btnStartStopVideo, + btnStopRecording, findViewById(R.id.btnSaveFolder), findViewById(R.id.btnResetPositionStill), findViewById(R.id.btnResetPositionVideo), @@ -181,6 +201,8 @@ class ActMain : AppCompatActivity(), View.OnClickListener { tvButtonSizeError = findViewById(R.id.tvButtonSizeError) tvButtonSizeError.vg(false) tvCodec = findViewById(R.id.tvCodec) + tvStatusStill = findViewById(R.id.tvStatusStill) + tvStatusVideo = findViewById(R.id.tvStatusVideo) val pref = App1.pref @@ -223,10 +245,15 @@ class ActMain : AppCompatActivity(), View.OnClickListener { else -> null } - val tvVideoError: TextView = findViewById(R.id.tvVideoError) - tvVideoError.vg(message != null)?.text = message videcCaptureEnabled = message == null + if (!videcCaptureEnabled) { + tvStatusVideo.setTextColor(ContextCompat.getColor(this, R.color.colorTextError)) + tvStatusVideo.text = message + + btnStopRecording.visibility = View.INVISIBLE + } + val tvLogToFileDesc: TextView = findViewById(R.id.tvLogToFileDesc) tvLogToFileDesc.text = getString( R.string.output_log_to_file_desc, @@ -280,8 +307,39 @@ class ActMain : AppCompatActivity(), View.OnClickListener { btnStartStopStill.isEnabledWithColor = !isCapturing btnStartStopVideo.isEnabledWithColor = !isCapturing && videcCaptureEnabled - } + btnStopRecording.isEnabledWithColor = CaptureServiceBase.isVideoCapturing() + + tvStatusStill.text = getString( + R.string.status_is, + when { + CaptureServiceStill.isAlive() -> + getString(R.string.status_running) + else -> + getString( + R.string.stopped_by, + CaptureServiceBase.getStopReason(CaptureServiceStill::class.java) + ?: "" + ) + } + ) + + if (videcCaptureEnabled) { + tvStatusVideo.text = getString( + R.string.status_is, + when { + CaptureServiceVideo.isAlive() -> + getString(R.string.status_running) + else -> + getString( + R.string.stopped_by, + CaptureServiceBase.getStopReason(CaptureServiceVideo::class.java) + ?: "" + ) + } + ) + } + } // 権限のチェックと取得インタラクションの開始 // 画面表示時や撮影ボタンの表示開始時に呼ばれる @@ -299,7 +357,11 @@ class ActMain : AppCompatActivity(), View.OnClickListener { timeStartButtonTappedStill = 0L ContextCompat.startForegroundService( this, - Intent(this, CaptureServiceStill::class.java) + Intent(this, CaptureServiceStill::class.java).apply { + Capture.screenCaptureIntent?.let { + putExtra(CaptureServiceBase.EXTRA_SCREEN_CAPTURE_INTENT, it) + } + } ) } @@ -310,7 +372,11 @@ class ActMain : AppCompatActivity(), View.OnClickListener { timeStartButtonTappedVideo = 0L ContextCompat.startForegroundService( this, - Intent(this, CaptureServiceVideo::class.java) + Intent(this, CaptureServiceVideo::class.java).apply { + Capture.screenCaptureIntent?.let { + putExtra(CaptureServiceBase.EXTRA_SCREEN_CAPTURE_INTENT, it) + } + } ) } } diff --git a/app/src/main/java/jp/juggler/screenshotbutton/Capture.kt b/app/src/main/java/jp/juggler/screenshotbutton/Capture.kt index 5001e13..1035ff5 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/Capture.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/Capture.kt @@ -24,14 +24,11 @@ import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.delay import kotlinx.coroutines.withContext import kotlinx.coroutines.withTimeoutOrNull -import java.util.* import java.util.concurrent.atomic.AtomicReference import kotlin.coroutines.resume import kotlin.coroutines.suspendCoroutine import kotlin.math.max import kotlin.math.min -import android.media.MediaCodec -import android.os.Bundle object Capture { @@ -54,7 +51,7 @@ object Capture { private lateinit var mediaProjectionManager: MediaProjectionManager private lateinit var windowManager: WindowManager - private var screenCaptureIntent: Intent? = null + var screenCaptureIntent: Intent? = null private var mediaProjection: MediaProjection? = null private var mediaProjectionAddr = AtomicReference(null) private var mediaProjectionState = MediaProjectionState.Off @@ -66,10 +63,8 @@ object Capture { fun onInitialize(context: Context) { log.d("onInitialize") mediaScannerTracker = MediaScannerTracker(context.applicationContext, handler) - mediaProjectionManager = - context.getSystemService(Context.MEDIA_PROJECTION_SERVICE) as MediaProjectionManager - windowManager = - context.getSystemService(Context.WINDOW_SERVICE) as WindowManager + mediaProjectionManager = systemService(context)!! + windowManager = systemService(context)!! } // mediaProjection と screenCaptureIntentを解放する @@ -107,23 +102,24 @@ object Capture { } fun handleScreenCaptureIntentResult( - activity: Activity, + context:Context, resultCode: Int, data: Intent? ): Boolean { log.d("handleScreenCaptureIntentResult") return when { resultCode != Activity.RESULT_OK -> { - log.eToast(activity, false, "permission not granted.") + log.eToast(context, false, "permission not granted.") release() } data == null -> { - log.eToast(activity, false, "result data is null.") + log.eToast(context, false, "result data is null.") release() } else -> { + log.i("screenCaptureIntent set!") // 得られたインテントはExtrasにBinderProxyオブジェクトを含む。ファイルに保存とかは無理っぽい… screenCaptureIntent = data mediaProjectionState = MediaProjectionState.HasScreenCaptureIntent diff --git a/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceBase.kt b/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceBase.kt index ecc63c0..16fd715 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceBase.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceBase.kt @@ -1,9 +1,7 @@ package jp.juggler.screenshotbutton import android.annotation.SuppressLint -import android.app.Notification -import android.app.NotificationManager -import android.app.Service +import android.app.* import android.content.Context import android.content.Intent import android.content.res.Configuration @@ -21,20 +19,22 @@ import java.util.* import kotlin.math.abs import kotlin.math.max -@SuppressLint("InflateParams") abstract class CaptureServiceBase( - val fpCameraButtonX: FloatPref, - val fpCameraButtonY: FloatPref, - @DrawableRes val startButtonId: Int, - val notificationId: Int + val isVideo: Boolean ) : Service(), View.OnClickListener, View.OnTouchListener { companion object { - private val log = LogCategory("${App1.tagPrefix}/CaptureServiceStill") + val logCompanion = LogCategory("${App1.tagPrefix}/CaptureService") + + const val EXTRA_SCREEN_CAPTURE_INTENT = "screenCaptureIntent" private var captureJob: WeakReference? = null - private fun isCapturing() = captureJob?.get()?.isActive == true + private var isVideoCaptureJob = false + + fun isCapturing() = captureJob?.get()?.isActive == true + + fun isVideoCapturing() = isCapturing() && isVideoCaptureJob private val serviceList = LinkedList>() @@ -55,11 +55,57 @@ abstract class CaptureServiceBase( fun showButtonAll() { runOnMainThread { - log.d("showButtonAll") + logCompanion.d("showButtonAll") getServices().forEach { it.showButton() } ActMain.getActivity()?.showButton() } } + + fun getStopReason(serviceClass: Class<*>): String? { + val key = "StopReason/${serviceClass.name}" + return App1.pref.getString(key, null) + } + + fun setStopReason(service: CaptureServiceBase, reason: String?) { + val key = "StopReason/${service.javaClass.name}" + App1.pref.edit().apply { + if (reason == null) { + remove(key) + } else { + putString(key, reason) + } + }.apply() + } + + + } + + private val log = LogCategory("${App1.tagPrefix}/${this.javaClass.simpleName}") + + val fpCameraButtonX = when { + isVideo -> Pref.fpCameraButtonXVideo + else -> Pref.fpCameraButtonXStill + } + + val fpCameraButtonY = when { + isVideo -> Pref.fpCameraButtonYVideo + else -> Pref.fpCameraButtonYStill + } + + @DrawableRes + val startButtonId = when { + isVideo -> R.drawable.ic_videocam + else -> R.drawable.ic_camera + } + + private val notificationId = when { + isVideo -> NOTIFICATION_ID_RUNNING_VIDEO + else -> NOTIFICATION_ID_RUNNING_STILL + } + + private val pendingIntentRequestCodeRestart = when { + isVideo -> PI_CODE_RESTART_VIDEO + else -> PI_CODE_RESTART_STILL } protected val context: Context @@ -85,24 +131,55 @@ abstract class CaptureServiceBase( @Volatile protected var isDestroyed = false -// private lateinit var serviceJob: Job -// override val coroutineContext: CoroutineContext -// get() = Dispatchers.Main + serviceJob - // serviceJob = Job() - override fun onBind(intent: Intent): IBinder? = null - override fun onStart(intent: Intent, startId: Int) { + override fun onStart(intent: Intent?, startId: Int) { + handleIntent(intent) } - override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { - return START_NOT_STICKY + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + handleIntent(intent) + return START_STICKY } + override fun onTaskRemoved(rootIntent: Intent?) { log.d("onTaskRemoved") + setStopReason(this, "onTaskRemoved") + + // restart service + systemService(this)?.let { manager -> + val pendingIntent = PendingIntent.getService( + this, + pendingIntentRequestCodeRestart, + Intent(this, this.javaClass) + .apply { + val old = rootIntent?.getParcelableExtra( + EXTRA_SCREEN_CAPTURE_INTENT + ) + if (old != null) putExtra(EXTRA_SCREEN_CAPTURE_INTENT, old) + }, + PendingIntent.FLAG_UPDATE_CURRENT + ) + + if (Build.VERSION.SDK_INT >= 23) { + manager.setExactAndAllowWhileIdle( + AlarmManager.RTC, + System.currentTimeMillis() + 500, + pendingIntent + ) + + } else { + manager.setExact( + AlarmManager.RTC, + System.currentTimeMillis() + 500, + pendingIntent + ) + } + + } + super.onTaskRemoved(rootIntent) - stopSelf() } override fun onLowMemory() { @@ -118,12 +195,16 @@ abstract class CaptureServiceBase( @SuppressLint("ClickableViewAccessibility", "RtlHardcoded") override fun onCreate() { log.d("onCreate") + App1.prepareAppState(context) addActiveService(this) + setStopReason(this, null) + super.onCreate() - App1.prepareAppState(context) - notificationManager = getNotificationManager() - windowManager = getSystemService(WINDOW_SERVICE) as WindowManager + notificationManager = systemService(context)!! + windowManager = systemService(context)!! + + @SuppressLint("InflateParams") viewRoot = LayoutInflater.from(context).inflate(R.layout.service_overlay, null) startForeground(notificationId, createRunningNotification(false)) @@ -156,21 +237,11 @@ abstract class CaptureServiceBase( btnCamera.windowLayoutParams = layoutParam windowManager.addView(viewRoot, layoutParam) - if (!isCapturing()) { - try { - Capture.updateMediaProjection() - } catch (ex: Throwable) { - log.eToast(this, ex, "updateMediaProjection failed.") - stopSelf() - return - } - } - showButtonAll() } override fun onDestroy() { - log.i("onDestroy start") + log.i("onDestroy start. stopReason=${getStopReason(this.javaClass)}") removeActiveService(this) isDestroyed = true windowManager.removeView(viewRoot) @@ -178,7 +249,7 @@ abstract class CaptureServiceBase( if (this is CaptureServiceVideo) Capture.stopVideo() - if (getServices().isEmpty() ) { + if (getServices().isEmpty()) { log.i("onDestroy: captureJob join start") runBlocking { captureJob?.get()?.join() @@ -203,7 +274,7 @@ abstract class CaptureServiceBase( Capture.updateMediaProjection() } catch (ex: Throwable) { log.eToast(this, ex, "updateMediaProjection failed.") - stopSelf() + stopWithReason("UpdateMediaProjectionFailedAtConfigurationChanged") } } } @@ -216,6 +287,14 @@ abstract class CaptureServiceBase( ////////////////////////////////////////////// + private fun handleIntent(intent: Intent?) { + val screenCaptureIntent = intent?.getParcelableExtra(EXTRA_SCREEN_CAPTURE_INTENT) + if (screenCaptureIntent != null && Capture.screenCaptureIntent == null) { + Capture.handleScreenCaptureIntentResult(this, Activity.RESULT_OK, screenCaptureIntent) + } + } + + // 設定からボタン位置を読み直す // ただし反映はされない private fun loadButtonPosition() { @@ -261,7 +340,7 @@ abstract class CaptureServiceBase( // キャプチャ中はボタンを隠す(操作できない) btnCamera.vg(!isCapturing) - val hideByTouching = getServices ().find{ it.hideByTouching } != null + val hideByTouching = getServices().find { it.hideByTouching } != null // タッチ中かつ非ドラッグ状態ならボタンを隠す(操作はできる) if (hideByTouching) { @@ -342,12 +421,16 @@ abstract class CaptureServiceBase( /////////////////////////////////////////////////////////////// - private fun createRunningNotification(isRecording: Boolean): Notification { - val notificationChannelId = createNotificationChannel() - return NotificationCompat.Builder(context, notificationChannelId) + val channelId = when { + isVideo -> NOTIFICATION_CHANNEL_VIDEO + else -> NOTIFICATION_CHANNEL_STILL + } + createNotificationChannel(channelId) + + return NotificationCompat.Builder(context, channelId) .setPriority(NotificationCompat.PRIORITY_LOW) .setOngoing(true) .apply { @@ -363,6 +446,11 @@ abstract class CaptureServiceBase( } + fun stopWithReason(reason: String) { + setStopReason(this, reason) + stopSelf() + } + fun captureStop() { Capture.stopVideo() } @@ -386,6 +474,7 @@ abstract class CaptureServiceBase( Capture.isCapturing = true showButtonAll() + isVideoCaptureJob = isVideo captureJob = WeakReference(GlobalScope.launch(Dispatchers.IO) { try { val captureResult = Capture.capture( @@ -394,7 +483,9 @@ abstract class CaptureServiceBase( isVideo = this@CaptureServiceBase is CaptureServiceVideo ) runOnMainThread { - afterCapture(captureResult) + if (Pref.bpShowPostView(App1.pref)) { + openPostView(captureResult) + } } } catch (ex: Throwable) { log.eToast(context, ex, "capture failed.") @@ -402,14 +493,31 @@ abstract class CaptureServiceBase( }) } - abstract fun createNotificationChannel(): String + + abstract fun createNotificationChannel(channelId: String) abstract fun arrangeNotification( builder: NotificationCompat.Builder, isRecording: Boolean ): IntArray - abstract fun afterCapture(captureResult: Capture.CaptureResult) - - + abstract fun openPostView(captureResult: Capture.CaptureResult) + +} + +// サービスが存在しなければ通知を消す +// 存在するならコールバックを実行する +fun T?.runOnService( + context: Context, + notificationId: Int? = null, + block: T.() -> R +): R? = when (this) { + null -> { + CaptureServiceBase.logCompanion.eToast(context, false, "service not running.") + if(notificationId!=null) { + systemService(context)?.cancel(notificationId) + } + null + } + else -> block.invoke(this) } \ No newline at end of file diff --git a/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceStill.kt b/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceStill.kt index 54b654f..a678ca9 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceStill.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceStill.kt @@ -7,12 +7,7 @@ import android.content.Intent import android.os.Build import androidx.core.app.NotificationCompat -class CaptureServiceStill : CaptureServiceBase( - Pref.fpCameraButtonXStill, - Pref.fpCameraButtonYStill, - startButtonId = R.drawable.ic_camera, - notificationId = NOTIFICATION_ID_RUNNING_STILL -) { +class CaptureServiceStill : CaptureServiceBase(isVideo = false) { companion object { fun getService() = getServices().find{ it is CaptureServiceStill} @@ -20,11 +15,11 @@ class CaptureServiceStill : CaptureServiceBase( fun isAlive() = getService() != null } - override fun createNotificationChannel() :String{ + override fun createNotificationChannel(channelId:String) { if (Build.VERSION.SDK_INT >= API_NOTIFICATION_CHANNEL) { notificationManager.createNotificationChannel( NotificationChannel( - NOTIFICATION_CHANNEL_STILL, + channelId, getString(R.string.capture_standby_still), NotificationManager.IMPORTANCE_LOW ).apply { @@ -32,8 +27,6 @@ class CaptureServiceStill : CaptureServiceBase( } ) } - - return NOTIFICATION_CHANNEL_STILL } override fun arrangeNotification( @@ -79,8 +72,8 @@ class CaptureServiceStill : CaptureServiceBase( return intArrayOf(0) } - override fun afterCapture(captureResult: Capture.CaptureResult){ - if (!isDestroyed && Pref.bpShowPostView(App1.pref)) { + override fun openPostView(captureResult: Capture.CaptureResult){ + if (!isDestroyed ) { ActViewer.open(context, captureResult.documentUri) } } diff --git a/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceVideo.kt b/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceVideo.kt index 88efe50..7d033a1 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceVideo.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/CaptureServiceVideo.kt @@ -7,23 +7,19 @@ import android.content.Intent import android.os.Build import androidx.core.app.NotificationCompat -class CaptureServiceVideo : CaptureServiceBase( - Pref.fpCameraButtonXVideo, - Pref.fpCameraButtonYVideo, - startButtonId = R.drawable.ic_videocam, - notificationId = NOTIFICATION_ID_RUNNING_VIDEO -) { +class CaptureServiceVideo : CaptureServiceBase(isVideo = true) { + companion object { fun getService() = getServices().find { it is CaptureServiceVideo } - fun isAlive() = getService() != null + } - override fun createNotificationChannel(): String { + override fun createNotificationChannel(channelId:String) { if (Build.VERSION.SDK_INT >= API_NOTIFICATION_CHANNEL) { notificationManager.createNotificationChannel( NotificationChannel( - NOTIFICATION_CHANNEL_VIDEO, + channelId, getString(R.string.capture_standby_video), NotificationManager.IMPORTANCE_LOW ).apply { @@ -31,7 +27,6 @@ class CaptureServiceVideo : CaptureServiceBase( } ) } - return NOTIFICATION_CHANNEL_VIDEO } override fun arrangeNotification( @@ -104,8 +99,8 @@ class CaptureServiceVideo : CaptureServiceBase( return intArrayOf(0, 1) } - override fun afterCapture(captureResult: Capture.CaptureResult) { - if (!isDestroyed && Pref.bpShowPostView(App1.pref) && captureResult.mediaUri != null) { + override fun openPostView(captureResult: Capture.CaptureResult) { + if (!isDestroyed && captureResult.mediaUri != null) { startActivity( Intent(Intent.ACTION_VIEW) .apply { diff --git a/app/src/main/java/jp/juggler/screenshotbutton/IDs.kt b/app/src/main/java/jp/juggler/screenshotbutton/IDs.kt index f5861ef..aa2170c 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/IDs.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/IDs.kt @@ -30,5 +30,8 @@ const val NOTIFICATION_ID_RUNNING_VIDEO = 2 const val PI_CODE_RUNNING_TAP = 0 const val PI_CODE_RUNNING_DELETE_STILL = 1 const val PI_CODE_RUNNING_DELETE_VIDEO = 2 -const val PI_CODE_VIDEO_START = 1 -const val PI_CODE_VIDEO_STOP = 2 +const val PI_CODE_VIDEO_START = 3 +const val PI_CODE_VIDEO_STOP = 4 +const val PI_CODE_RESTART_STILL = 5 +const val PI_CODE_RESTART_VIDEO = 6 + diff --git a/app/src/main/java/jp/juggler/screenshotbutton/MyReceiver.kt b/app/src/main/java/jp/juggler/screenshotbutton/MyReceiver.kt index b13792c..e3f33f8 100755 --- a/app/src/main/java/jp/juggler/screenshotbutton/MyReceiver.kt +++ b/app/src/main/java/jp/juggler/screenshotbutton/MyReceiver.kt @@ -4,47 +4,37 @@ import android.content.BroadcastReceiver import android.content.Context import android.content.Intent import jp.juggler.util.LogCategory -import jp.juggler.util.getNotificationManager class MyReceiver : BroadcastReceiver() { companion object { private val log = LogCategory("${App1.tagPrefix}/MyReceiver") + const val ACTION_RUNNING_DELETE_STILL = "running_delete_still" const val ACTION_RUNNING_DELETE_VIDEO = "running_delete_video" const val ACTION_RUNNING_VIDEO_START = "video_start" const val ACTION_RUNNING_VIDEO_STOP = "video_stop" - // サービスが存在しなければ通知を消す - // 存在するならコールバックを実行する - private fun T?.runOnService( - context: Context, - notificationId: Int, block: T.() -> R - ): R? = when (this) { - null -> { - log.eToast(context, false, "service not running.") - context.getNotificationManager().cancel(notificationId) - null - } - else -> block.invoke(this) - } + } override fun onReceive(context: Context, data: Intent?) { + + App1.prepareAppState(context) + val action = data?.action log.i("onReceive $action") when (data?.action) { - ACTION_RUNNING_DELETE_STILL -> CaptureServiceStill.getService() .runOnService(context, NOTIFICATION_ID_RUNNING_STILL) { - stopSelf() + stopWithReason("NotificationDeleteAction") } ACTION_RUNNING_DELETE_VIDEO -> CaptureServiceVideo.getService() .runOnService(context, NOTIFICATION_ID_RUNNING_VIDEO) { - stopSelf() + stopWithReason("NotificationDeleteAction") } ACTION_RUNNING_VIDEO_START -> diff --git a/app/src/main/java/jp/juggler/util/LogCategory.kt b/app/src/main/java/jp/juggler/util/LogCategory.kt index 84b38d1..94b142a 100755 --- a/app/src/main/java/jp/juggler/util/LogCategory.kt +++ b/app/src/main/java/jp/juggler/util/LogCategory.kt @@ -141,9 +141,9 @@ class LogCategory(private val tag: String) { synchronized(writer) { writer.appendHeader(lv, tag) .append(msg) - .append(" : ") + .append(", ") .append(error.javaClass.simpleName) - .append(' ') + .append(", ") .println(error.message ?: "?") error.printStackTrace(writer) writer.flush() diff --git a/app/src/main/java/jp/juggler/util/StorageUtils.kt b/app/src/main/java/jp/juggler/util/StorageUtils.kt index 4d4b938..d15eb49 100755 --- a/app/src/main/java/jp/juggler/util/StorageUtils.kt +++ b/app/src/main/java/jp/juggler/util/StorageUtils.kt @@ -95,7 +95,7 @@ private fun getVolumePathApi24(storageManager: StorageManager, uuid: String): St }?.let { // API 29 だとグレーリストに入っていた // Accessing hidden method Landroid/os/storage/StorageVolume;->getPath()Ljava/lang/String; (greylist, reflection, allowed) - StorageVolume::class.java.getMethod("getPath").invoke(it) as? String + StorageVolume::class.java.getMethod("getPath").invoke(it).castOrNull() } ?: error("can't find volume for uuid $uuid") @TargetApi(21) @@ -108,48 +108,65 @@ private fun getVolumePathApi21(storageManager: StorageManager, uuid: String): St } else -> { - val volumes = storageManager.javaClass.getMethod("getVolumeList").invoke(storageManager) - as? Array<*> ?: error("storageManager.getVolumeList() failed.") - if (volumes.isEmpty()) error("storageManager.getVolumeList() is empty.") - val volumeClass = volumes[0]?.javaClass ?: error("volumes[0].javaClass is null.") + + val volumes = storageManager.javaClass.getMethod("getVolumeList") + .invoke(storageManager) + .castOrNull>() + ?: error("storageManager.getVolumeList() failed.") + + if (volumes.isEmpty()) + error("storageManager.getVolumeList() is empty.") + + val volumeClass = volumes.first()?.javaClass + ?: error("volumes[0].javaClass is null.") + val getUuid = volumeClass.getMethod("getUuid") + val volume = volumes.find { it ?: return@find false - uuid == getUuid.invoke(it) as? String - } ?: "missing volume for uuid $uuid" - val state = volumeClass.getMethod("getState").invoke(volume) as? String - if (state != "mounted") error("uuid is not mounted. $state") + uuid == getUuid.invoke(it) + } ?: error("missing volume for uuid $uuid") + + val state = volumeClass.getMethod("getState") + .invoke(volume) + .castOrNull() - (volumeClass.getMethod("getPath").invoke(volume) as? String).notEmpty() + if (state != "mounted") + error("uuid is not mounted. $state") + + volumeClass.getMethod("getPath") + .invoke(volume) + .castOrNull() + .notEmpty() ?: error("volume.getPath() failed.") } } -fun pathFromDocumentUri(context: Context, uri: Uri): String? { - return try { - when { +fun pathFromDocumentUri(context: Context, uri: Uri): String? = try { + when { - uri.authority == "file" -> return uri.path + uri.authority == "file" -> uri.path - isExternalStorageDocument(uri) -> { - val split = getDocumentId(uri).split(":").dropLastWhile { it.isEmpty() } - if (split.size < 2) error("document id has no semicolon.") - val storageManager = - context.getSystemService(Context.STORAGE_SERVICE) as StorageManager - val volumePath = if (Build.VERSION.SDK_INT >= API_STORAGE_VOLUME) { - getVolumePathApi24(storageManager, split[0]) - } else { - getVolumePathApi21(storageManager, split[0]) - } - "$volumePath/${split[1]}" + isExternalStorageDocument(uri) -> { + val split = getDocumentId(uri).split(":").dropLastWhile { it.isEmpty() } + if (split.size < 2) error("document id has no semicolon.") + + val storageManager = systemService(context)!! + + val volumePath = if (Build.VERSION.SDK_INT >= API_STORAGE_VOLUME) { + getVolumePathApi24(storageManager, split[0]) + } else { + getVolumePathApi21(storageManager, split[0]) } - else->error("pathFromDocumentUri: not supported $uri") + "$volumePath/${split[1]}" } - } catch (ex: Throwable) { - log.e( ex, "pathFromDocumentUri failed. $uri") - null + + else -> error("pathFromDocumentUri: not supported $uri") } +} catch (ex: Throwable) { + log.e(ex, "pathFromDocumentUri failed. $uri") + null } @@ -177,9 +194,9 @@ fun generateDocument( fun deleteDocument( context: Context, itemUri: Uri -): Boolean{ +): Boolean { // 削除に成功したら真 - if(DocumentsContract.deleteDocument(context.contentResolver, itemUri)) + if (DocumentsContract.deleteDocument(context.contentResolver, itemUri)) return true // 削除できない場合、存在しないなら真 @@ -190,7 +207,7 @@ fun deleteDocument( null, null )?.use { - if(it.count==0) return true + if (it.count == 0) return true } // 削除できないが存在はしている… diff --git a/app/src/main/java/jp/juggler/util/Utils.kt b/app/src/main/java/jp/juggler/util/Utils.kt index 916bdf4..0cac756 100755 --- a/app/src/main/java/jp/juggler/util/Utils.kt +++ b/app/src/main/java/jp/juggler/util/Utils.kt @@ -1,9 +1,9 @@ package jp.juggler.util import android.app.AppOpsManager -import android.app.NotificationManager import android.content.Context import android.graphics.* +import android.hardware.display.DisplayManager import android.media.MediaCodec import android.net.Uri import android.os.Binder.getCallingUid @@ -13,9 +13,10 @@ import android.os.Handler import android.os.Looper import android.provider.Settings import android.util.DisplayMetrics +import android.view.Display import android.view.View import android.view.WindowManager -import android.widget.Button +import androidx.core.content.ContextCompat.getSystemService import jp.juggler.screenshotbutton.API_APPLICATION_OVERLAY import java.util.* @@ -33,8 +34,8 @@ fun Float.dp2px(dm: DisplayMetrics) = fun Int.px2dp(dm: DisplayMetrics) = this.toFloat() / dm.density -fun T?.vg(visible: Boolean): T? { - this?.visibility = if (visible) View.VISIBLE else View.GONE +fun T?.vg(visible: Boolean): T? { + this?.visibility = if (visible) View.VISIBLE else View.GONE return if (visible) this else null } @@ -70,6 +71,15 @@ fun createResizedBitmap(src: Bitmap, dstWidth: Int, dstHeight: Int): Bitmap { ?: error("createBitmap returns null") } +// 型推論できる文脈だと型名を書かずにすむ +@Suppress("unused") +inline fun Any?.cast(): T = this as T + +inline fun Any?.castOrNull(): T? = this as? T + +// 型推論できる文脈だと型名を書かずにすむ +inline fun systemService(context: Context): T? = + /* ContextCompat. */ getSystemService(context, T::class.java) // Android 8.0 は Settings.canDrawOverlays(context) にバグがある // https://stackoverflow.com/questions/46173460/why-in-android-o-method-settings-candrawoverlays-returns-false-when-user-has @@ -86,46 +96,44 @@ fun canDrawOverlaysCompat(context: Context): Boolean { if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { // AppOpsManager.checkOp() は API 29 でdeprecated - (context.getSystemService(Context.APP_OPS_SERVICE) as? AppOpsManager) - ?.let { manager -> - try { - @Suppress("DEPRECATION") - val result = manager.checkOp( - AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, - getCallingUid(), - context.packageName - ) - return result == AppOpsManager.MODE_ALLOWED - } catch (_: Throwable) { - } - } - } - - //id this fails, we definitely can't do it - (context.getSystemService(Context.WINDOW_SERVICE) as? WindowManager) - ?.let { manager -> + systemService(context)?.let { manager -> try { - val viewToAdd = View(context) - val params = WindowManager.LayoutParams( - 0, - 0, - if (Build.VERSION.SDK_INT >= API_APPLICATION_OVERLAY) { - WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY - } else { - @Suppress("DEPRECATION") - WindowManager.LayoutParams.TYPE_SYSTEM_ALERT - }, - WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSPARENT + @Suppress("DEPRECATION") + val result = manager.checkOp( + AppOpsManager.OPSTR_SYSTEM_ALERT_WINDOW, + getCallingUid(), + context.packageName ) - viewToAdd.layoutParams = params - manager.addView(viewToAdd, params) - manager.removeView(viewToAdd) - return true + return result == AppOpsManager.MODE_ALLOWED } catch (_: Throwable) { } } + } + + //id this fails, we definitely can't do it + systemService(context)?.let { manager -> + try { + val viewToAdd = View(context) + val params = WindowManager.LayoutParams( + 0, + 0, + if (Build.VERSION.SDK_INT >= API_APPLICATION_OVERLAY) { + WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY + } else { + @Suppress("DEPRECATION") + WindowManager.LayoutParams.TYPE_SYSTEM_ALERT + }, + WindowManager.LayoutParams.FLAG_NOT_TOUCHABLE or + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT + ) + viewToAdd.layoutParams = params + manager.addView(viewToAdd, params) + manager.removeView(viewToAdd) + return true + } catch (_: Throwable) { + } + } return false @@ -141,8 +149,16 @@ fun runOnMainThread(block: () -> Unit) { } fun getScreenSize(context: Context) = Point().also { - val windowManager = context.getSystemService(Context.WINDOW_SERVICE) as WindowManager - windowManager.defaultDisplay.getRealSize(it) + + // systemService(context)!! +// .defaultDisplay!! +// .getRealSize(it) + + // https://github.com/google/grafika/blob/master/app/src/main/java/com/android/grafika/ScreenRecordActivity.java + // grafika のサンプルでは DisplayManager.getDisplay を使っていた + systemService(context)!! + .getDisplay(Display.DEFAULT_DISPLAY)!! + .getRealSize(it) } fun MediaCodec.suspend(suspend: Boolean) = @@ -162,11 +178,11 @@ fun String?.toUriOrNull() = null } -var View.isEnabledWithColor : Boolean +var View.isEnabledWithColor: Boolean get() = isEnabled - set(value){ + set(value) { isEnabled = value - alpha = if(value) 1f else 0.5f + alpha = if (value) 1f else 0.5f } fun getCurrentTimeString(): String { @@ -181,6 +197,3 @@ fun getCurrentTimeString(): String { , cal.get(Calendar.SECOND) ) } - -fun Context.getNotificationManager()= - getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager diff --git a/app/src/main/res/layout/act_main.xml b/app/src/main/res/layout/act_main.xml index 2a32ef6..32a1c47 100755 --- a/app/src/main/res/layout/act_main.xml +++ b/app/src/main/res/layout/act_main.xml @@ -13,95 +13,98 @@ android:layout_height="wrap_content" android:orientation="vertical" android:paddingStart="12dp" - android:paddingTop="50dp" + android:paddingTop="20dp" android:paddingEnd="12dp" android:paddingBottom="6dp"> +