Android uses Kotlin and CameraX to take photos without feeling on the desktop and screen (with source code)
Requirements: realize a silent photographing without displaying the interface, and can also switch the front and rear cameras and take photos through the volume keys in the rest screen state. After the photos are successfully saved, there is a vibration prompt. You can close the app of the service by clicking the app notification bar
Download the source code address at the end
Note: this blog is only written to provide you with some solutions. It only provides code, not executable apk. If there are errors, please point them out. Thank you
catalogue
(1) Mainly apply for permission and open service
(2) CameraX binding and service for PreviewViewLifecycle binding
(4) How to control the volume when the screen is still resting
(5) Open the service vibration prompt and the successful vibration prompt
(6) How to turn off the service by clicking the event in the notification bar
(1) Mainly apply for permission and open service
According to the conditions, we need to be in Android manifest Declare permissions in XML
<!-- Suspended window permission --> <uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" /> <!-- Camera permissions --> <uses-permission android:name="android.permission.CAMERA" /> <uses-feature android:name="android.hardware.camera.any" /> <!-- Vibration authority --> <uses-permission android:name="android.permission.VIBRATE" /> <!-- File write permission --> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <!-- Notification bar permissions --> <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
Among them, we need to dynamically apply for two permissions: camera permission and floating window permission
Let's first judge whether the camera permission has been applied for at the beginning of MainActivity
MainActivity class //Is camera permission turned on var mIsAlreadyOpenCameraPermission: Boolean = false mIsAlreadyOpenCameraPermission = !ActivityCompat.shouldShowRequestPermissionRationale( this, Manifest.permission.CAMERA )
If we haven't applied, we'll apply
MainActivity class //Camera permission request code val CAMERA_PERMISSION_REQUEST_CODE = 0 ActivityCompat.requestPermissions( this, arrayOf<String>(Manifest.permission.CAMERA), CAMERA_PERMISSION_REQUEST_CODE )
When the camera permission application is successful, we will enter onRequestPermissionsResult (...) Go inside and continue to apply for permission to suspend the window
MainActivity class if (requestCode == CAMERA_PERMISSION_REQUEST_CODE) { if (grantResults[0] == PackageManager.PERMISSION_GRANTED) { if (!mIsAlreadyOpenFloatPermission) { requestFloatWindowPermisson() } } else { failToastAndFinish() } }
MainActivity class //Request suspended window permission fun requestFloatWindowPermisson() { val ACTION_MANAGE_OVERLAY_PERMISSION = "android.settings.action.MANAGE_OVERLAY_PERMISSION" val intent = Intent( ACTION_MANAGE_OVERLAY_PERMISSION, Uri.parse("package:" + getPackageName()) ) startActivityForResult(intent, FLOW_PERMISSION_REQUEST_CODE) }
Then in startActivityForResult (...) Method to see whether the user has opened the permission. If the permission is given, open the suspended window service. Otherwise, close the activity and give an error prompt
MainActivity class val isOpen: Boolean = Settings.canDrawOverlays(this) if (isOpen) { //open startWindowService() } else { //close Toast.makeText(this, "Failed to open. Please open the permission manually", Toast.LENGTH_SHORT).show() }
Then we open the service. First, we need to create an invisible floating window
In the FlowWindowService class
New layout_ flaot_ The layout of the window suspension window is a camera preview view with a content of 0.1dp (a large DP can be set during debugging)
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="0.1dp" android:layout_height="0.1dp"> <androidx.camera.view.PreviewView android:id="@+id/viewFinder" android:layout_width="0.1dp" android:layout_height="0.1dp" /> </FrameLayout>
And initialize the suspended window layout
FlowWindowService class lateinit var mWindowManager: WindowManager lateinit var mWmParams: WindowManager.LayoutParams lateinit var mInflater: LayoutInflater lateinit var mPreviewView: PreviewView lateinit var mFloatingLayout: View mWindowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager mWmParams = getParams() mInflater = LayoutInflater.from(getApplicationContext()) mFloatingLayout = mInflater.inflate(R.layout.layout_float_window, null) mPreviewView = mFloatingLayout!!.findViewById<PreviewView>(R.id.viewFinder); mWindowManager.addView(mFloatingLayout, mWmParams)
(2) CameraX binding to PreviewView and LifecycleService
Now that the layout file is written, the corresponding contents are initialized
Then we can write a CameraManage class to uniformly manage the api of CameraX
First, we need to get some required parameters from the service, so we request the methods of context and lifecycle owner parameters on the constructor
CameraManage class constructor (context: Context, lifecycleOwner: LifecycleOwner) { this.context = context this.mLifecycleOwner = lifecycleOwner }
We know that when CameraX is used, it is bound with Lifecycle, which can automatically help you take over the life cycle of camera
public Camera bindToLifecycle(@NonNull LifecycleOwner lifecycleOwner, @NonNull CameraSelector cameraSelector, @NonNull UseCase... useCases)
But our camera runs on the service. How can we make the service get the object of lifecycle owner
Don't worry, Google already has the method of lifecycle service, but it is in the extension package, so we need to refer to the corresponding package first
implementation "android.arch.lifecycle:extensions:1.1.1"
Then our service inherits from LifecycleService () and can reference the corresponding LifecycleOwner
class FlowWindowService : LifecycleService()
The next step is to open the camera code to display the contents of the camera on our suspension box. The first parameter is the corresponding View, and the second is the selection of the camera (front or rear)
CameraManage class fun startCamera(viewFinder: PreviewView, cameraSelector: CameraSelector) { val cameraProviderFuture = ProcessCameraProvider.getInstance(context) cameraProviderFuture.addListener(Runnable { val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() val preview: UseCase = Preview.Builder() .build() .also { it.setSurfaceProvider(viewFinder.createSurfaceProvider()) } mImageCapture = ImageCapture.Builder().setCaptureMode(ImageCapture.CAPTURE_MODE_MINIMIZE_LATENCY) .build() try { cameraProvider.unbindAll() cameraProvider.bindToLifecycle( mLifecycleOwner, cameraSelector, preview, mImageCapture ) } catch (exc: Exception) { Log.e(TAG, "Use case binding failed", exc) } }, ContextCompat.getMainExecutor(context)) }
Then our service can call this method
FlowWindowService class mCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA mMyCameraManage.startCamera( mPreviewView, mCameraSelector )
In this way, we can see the preview of the camera on the floating window (I resized the PreviewView to show the effect)
Then the next step is to take photos. Write a takePicture method and a callback method, and then take the callback method as a parameter to return the success of taking photos to the caller to process the content
CameraManage class fun takePicture(onTakePictureCallBack: OnTakePictureCallBack) { mImageCapture!!.takePicture( outputOptions, ContextCompat.getMainExecutor(context), object : ImageCapture.OnImageSavedCallback { override fun onError(exc: ImageCaptureException) { onTakePictureCallBack.onFailCallBack() } override fun onImageSaved(output: ImageCapture.OutputFileResults) { onTakePictureCallBack.onSuccessCallBack() } }) } //Camera callback interface OnTakePictureCallBack { fun onFailCallBack() fun onSuccessCallBack() }
Then we can call in the FlowWindowService class.
FlowWindowService class mCameraManage.takePicture(mMyOnTakePictureCallBack)
(3) Click the volume key on the desktop to control whether to call the front camera or the rear camera
We are all familiar with listening to physical keys on Activity or Fragment. We only need to rewrite onKeyDown method, but our program runs on the service
There is no such method to rewrite and intercept the event that the volume key cannot be clicked (please share it secretly if there is one, thank you). Therefore, we have to find another way to judge the volume key press by monitoring the volume every second
Create a new VulomeManage class to mainly monitor the volume value and define a timer
VolumeManage class var mTimer: Timer? = null //0 = no change greater than 1 = volume increase less than - 1 = volume decrease var mVolumeStatus = 0 //Volume value var mVolumeValue = 0
Another problem is that we actually don't want to change the volume value. Our purpose is to press the volume key to take pictures. On the one hand, it will reach the maximum or minimum value, resulting in invalid judgment conditions at extreme values, so we can't take pictures. On the other hand, it doesn't conform to humanized settings
So when the service is running, we find that the volume value has changed, and we need to reset it back to the original volume
We first define a judgment variable mIsReset to judge whether to reset back to the original variable, and then we judge whether the volume has changed according to the values of the previous second and the current volume
VolumeManage class var mIsReset = false private fun startVolumeListenThread() { mTimer = fixedRateTimer(TAG, false, 0, 1000) { //Current volume value var nowVolume = mAudioManager!!.getStreamVolume(STREAM_MUSIC) //Initial volume value var originVolume = mVolumeValue mVolumeStatus = nowVolume - mVolumeValue mVolumeValue = nowVolume //Equal to 0 indicates that the volume has not changed if (mVolumeStatus != 0) { //Volume increase Log.e( TAG, "startVolumeListenThread: Monitor volume current volume ${mVolumeValue} Volume status ${mVolumeStatus} mIsReset ${mIsReset}" ) if (mVolumeStatus >= 1) { //Increasing the volume indicates that you clicked the volume key mIsReset = true mAudioVolumeListner!!.volumeUp() } else { mIsReset = true mAudioVolumeListner!!.volumeDown() } if (mIsReset) { //Reset the volume. Because you don't want to really change the volume, restore the volume to its previous state mVolumeValue = originVolume mAudioManager!!.setStreamVolume( AudioManager.STREAM_MUSIC, mVolumeValue, AudioManager.FLAG_PLAY_SOUND ) mIsReset = false } } } }
Then add a listener to let the caller know the state of the volume, so as to realize the corresponding content
VolumeManage class fun start() { if (mAudioVolumeListner != null) { startVolumeListenThread() } } fun setAudioVolumeListner(audioVolumeListner: AudioVolumeListner) { mAudioVolumeListner = audioVolumeListner } interface AudioVolumeListner { fun volumeUp() fun volumeDown() }
We return to the FlowWindowService class to call the corresponding content, so we can turn on the corresponding camera in the anonymous internal class
FlowWindowService class private fun initAudioListener() { mVolumeManage.setAudioVolumeListner(object : VolumeManage.AudioVolumeListner { override fun volumeUp() { //Turn on the front camera Log.e(TAG, "volumeUp: ") if (mCameraSelector != CameraSelector.DEFAULT_FRONT_CAMERA) { mCameraSelector = CameraSelector.DEFAULT_FRONT_CAMERA mCameraManage.startCamera(mPreviewView, mCameraSelector) } mCameraManage.takePicture(mMyOnTakePictureCallBack) } override fun volumeDown() { //Turn on the rear camera Log.e(TAG, "volumeDown: ") if (mCameraSelector != CameraSelector.DEFAULT_BACK_CAMERA) { mCameraSelector = CameraSelector.DEFAULT_BACK_CAMERA mCameraManage.startCamera(mPreviewView, mCameraSelector) } mCameraManage.takePicture(mMyOnTakePictureCallBack) } }) mVolumeManage.start() }
However, we found that if we click the volume button in the state of mobile phone screen, our volume will not change
(4) How to control the volume when the screen is still resting
In fact, the method is also very simple, that is, we can play an audio file without sound in the background, and then press the volume key in the state of rest screen, which is still effective
First create the SilentMediaManage class, then create a MediaPlayer instance, put a silent audio file, and then set it to cycle
SilentMediaManage class private fun init() { mMediaPlayer = MediaPlayer.create(context.applicationContext, R.raw.silent) //Start cycle mMediaPlayer?.setLooping(true) }
Then create a playback method
SilentMediaManage class fun startPlayMusic() { mMediaPlayer?.start() }
Then our FlowWindowService can call its method
FlowWindowService class override fun onCreate() { super.onCreate() mSilentMediaManage = SilentMediaManage(this) mSilentMediaManage.startPlayMusic() }
(5) Open the service vibration prompt and the successful vibration prompt
This is a relatively simple way to create the EffectManage class
Initialize mVibrator
EffectManage class lateinit var mVibrator: Vibrator mVibrator = context.getSystemService(Context.VIBRATOR_SERVICE) as Vibrator
Then expose the effect() method
EffectManage class // Duration of vibration Intensity of vibration fun effect(effectLong: Long, effectStrong: Int) { //shock val effect: VibrationEffect = if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { VibrationEffect.createOneShot(effectLong, effectStrong) } else { TODO("VERSION.SDK_INT < O") } mVibrator.vibrate(effect) }
We can call this method when we start the service and take photos successfully or fail
FlowWindowService class //Duration of vibration var effectLong: Long = 200 //The strength of the shock at success var effectSuccesStrong = 10 //Strength of vibration at failure var effectFailStrong = 100 override fun onCreate() { super.onCreate() //Vibration reminder service has been turned on mEffectManage.effect(effectLong, 20) } inner class MyOnTakePictureCallBack : CameraManage.OnTakePictureCallBack { override fun onFailCallBack() { mEffectManage.effect(effectLong, effectFailStrong) } override fun onSuccessCallBack() { //Vibration indicates completion mEffectManage.effect(effectLong, effectSuccesStrong) } }
(6) How to turn off the service by clicking the event in the notification bar
Create NotificationManage class
NotificationManage class constructor(context: Context) { this.context = context init() } fun init() { mNotificationManager = context.getSystemService(NOTIFICATION_SERVICE) as NotificationManager?; }
Let's create a PendingIntent first
PendingIntent is an encapsulation of Intent, but it does not execute a certain behavior immediately, but only after certain conditions are met or certain events are triggered
NotificationManage class private fun createIntent(): PendingIntent { val intent = Intent(context, QuitActivity::class.java) return PendingIntent.getActivity( context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT ) }
Our purpose in this scenario is to jump to the QuitActivity calling the exit service
Then we bulidNotification and set the pending intent just created
NotificationManage class private fun bulidNotification(pendingIntent: PendingIntent) { if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) { mNotification = Notification.Builder(context, CHANNEL_ID) .setSmallIcon(R.mipmap.ic_launcher) //Make the notification bar non slidable .setOngoing(true) .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) .setContentTitle(CONTENT_TITLE) .setContentText(CONTENT_TEXT) .setWhen(System.currentTimeMillis()) .setContentIntent(pendingIntent) .build() //You need more than channel 8 notifications to receive val importance = NotificationManager.IMPORTANCE_HIGH createNotificationChannel(CHANNEL_ID, CHANNEL_NAME, importance) } else { mNotification = Notification.Builder(context) .setSmallIcon(R.mipmap.ic_launcher) .setOngoing(true) .setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) .setContentTitle(CONTENT_TITLE) .setContentText(CONTENT_TEXT) .setWhen(System.currentTimeMillis()) .setContentIntent(pendingIntent) .build() } }
At this time, when we click the notification bar, we will jump to QuitActivity. QuitActivity will call StopService and systemt The exit () method ends the program
QuitActivity class stopService(Intent(this, FlowWindowService::class.java)) finish() System.exit(0)