Use Kotlin and CameraX to take photos without feeling on the desktop and screen (with source code)

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

(3) Click the volume key on the desktop to control whether to call the front camera or the rear camera

(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)

I hope this article can help you. Let's work together! If you have any questions, you can write to me privately. I will reply as long as the question is not too outrageous

Source code download address: https://download.csdn.net/download/JunJ19/17533340?spm=1001.2014.3001.5503

Keywords: Android Design Pattern kotlin security

Added by parms on Sat, 19 Feb 2022 16:17:13 +0200