Skip to main content

Android System


📸 Camera Usage Method

The following example shows how to use the Camera2 API to call the Android camera and perform preview.


1. Add Permissions

Add the required permissions in AndroidManifest.xml:

<uses-permission android:name="android.permission.CAMERA" />
<uses-feature android:name="android.hardware.camera" />
<uses-feature android:name="android.hardware.camera.autofocus" />

2. Check Camera Permissions

After installing the App, you can enter the system settings and authorize the App's camera access permission by default to avoid prompts during use.


3. Create Camera2 Preview

Use Kotlin for development, create CameraActivity.kt:

import android.Manifest
import android.content.pm.PackageManager
import android.graphics.SurfaceTexture
import android.hardware.camera2.*
import android.os.*
import android.util.Log
import android.view.Surface
import android.view.TextureView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.app.ActivityCompat

class CameraActivity : AppCompatActivity() {

private lateinit var textureView: TextureView
private lateinit var cameraManager: CameraManager

private var cameraDevice: CameraDevice? = null
private var captureSession: CameraCaptureSession? = null
private var previewRequestBuilder: CaptureRequest.Builder? = null
private var currentCameraId: String? = null

private val mainHandler = Handler(Looper.getMainLooper())

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
textureView = TextureView(this)
setContentView(textureView)

cameraManager = getSystemService(CAMERA_SERVICE) as CameraManager
textureView.surfaceTextureListener = textureListener
}

// TextureView Status Listener
private val textureListener = object : TextureView.SurfaceTextureListener {
override fun onSurfaceTextureAvailable(surface: SurfaceTexture, width: Int, height: Int) {
Log.i(TAG, "SurfaceTexture available: ${width}x$height")
openCamera()
registerCameraAvailabilityCallback()
}

override fun onSurfaceTextureSizeChanged(surface: SurfaceTexture, width: Int, height: Int) {
Log.i(TAG, "SurfaceTexture size changed: ${width}x$height")
}

override fun onSurfaceTextureDestroyed(surface: SurfaceTexture): Boolean {
Log.i(TAG, "SurfaceTexture destroyed")
closeCamera()
return true // Return true means we no longer use this SurfaceTexture
}

override fun onSurfaceTextureUpdated(surface: SurfaceTexture) {}
}

// Register Camera Availability Listener
private fun registerCameraAvailabilityCallback() {
cameraManager.registerAvailabilityCallback(object : CameraManager.AvailabilityCallback() {
override fun onCameraAvailable(cameraId: String) {
Log.i(TAG, "Camera available: $cameraId")
}

override fun onCameraUnavailable(cameraId: String) {
Log.w(TAG, "Camera unavailable: $cameraId")
}
}, mainHandler)
}

// Open Front Camera
private fun openCamera() {
try {
for (cameraId in cameraManager.cameraIdList) {
val characteristics = cameraManager.getCameraCharacteristics(cameraId)
val facing = characteristics.get(CameraCharacteristics.LENS_FACING)
if (facing == CameraCharacteristics.LENS_FACING_FRONT) {
currentCameraId = cameraId
break
}
}

if (currentCameraId == null) {
Log.e(TAG, "No camera found")
return
}

if (ActivityCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA), 1)
return
}

Log.i(TAG, "Opening camera: $currentCameraId")
cameraManager.openCamera(currentCameraId!!, stateCallback, mainHandler)

} catch (e: Exception) {
Log.e(TAG, "openCamera error: ${e.message}", e)
}
}

// Camera State Callback
private val stateCallback = object : CameraDevice.StateCallback() {
override fun onOpened(camera: CameraDevice) {
Log.i(TAG, "Camera opened: ${camera.id}")
cameraDevice = camera
startPreview()
}

override fun onDisconnected(camera: CameraDevice) {
Log.w(TAG, "Camera disconnected: ${camera.id}")
camera.close()
cameraDevice = null
}

override fun onError(camera: CameraDevice, error: Int) {
Log.e(TAG, "Camera error ($error): ${camera.id}")
camera.close()
cameraDevice = null
}
}

// Start Preview
private fun startPreview() {
val camera = cameraDevice ?: return
val surfaceTexture = textureView.surfaceTexture ?: return

try {
surfaceTexture.setDefaultBufferSize(1920, 1080)
val surface = Surface(surfaceTexture)

previewRequestBuilder =
camera.createCaptureRequest(CameraDevice.TEMPLATE_PREVIEW).apply {
addTarget(surface)
set(CaptureRequest.CONTROL_MODE, CameraMetadata.CONTROL_MODE_AUTO)
}

camera.createCaptureSession(listOf(surface),
object : CameraCaptureSession.StateCallback() {
override fun onConfigured(session: CameraCaptureSession) {
Log.i(TAG, "CaptureSession configured")
captureSession = session
try {
session.setRepeatingRequest(previewRequestBuilder!!.build(), null, mainHandler)
} catch (e: CameraAccessException) {
Log.e(TAG, "setRepeatingRequest error: ${e.message}", e)
}
}

override fun onConfigureFailed(session: CameraCaptureSession) {
Log.e(TAG, "CaptureSession configure failed")
}
},
mainHandler
)
} catch (e: Exception) {
Log.e(TAG, "startPreview error: ${e.message}", e)
}
}

// Close Camera Resources
private fun closeCamera() {
try {
captureSession?.close()
captureSession = null
cameraDevice?.close()
cameraDevice = null
} catch (e: Exception) {
Log.e(TAG, "closeCamera error: ${e.message}", e)
}
}

override fun onPause() {
super.onPause()
Log.i(TAG, "onPause: closing camera")
closeCamera()
}

override fun onResume() {
super.onResume()
Log.i(TAG, "onResume: try reopen camera")
if (textureView.isAvailable) {
openCamera()
}
}

override fun onDestroy() {
super.onDestroy()
Log.i(TAG, "onDestroy: releasing camera")
closeCamera()
}

companion object {
private const val TAG = "CameraActivity"
}
}

🎙 Microphone Usage Method

Using the microphone for audio capture can typically be achieved through MediaRecorder or AudioRecord. MediaRecorder is more suitable for audio recording, while AudioRecord provides lower-level control, suitable for scenarios requiring fine-grained control over audio capture.


Method 1: Simple Audio Recording Using MediaRecorder

MediaRecorder is a high-level interface, suitable for audio recording operations, simple and easy to use.

Step 1: Add Permissions

Add recording permissions in the AndroidManifest.xml file:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />

Step 2: Initialize MediaRecorder

Create and configure MediaRecorder in the code:

public class AudioRecorderActivity extends AppCompatActivity {
private MediaRecorder mediaRecorder;
private String audioFilePath;

@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_recorder);
// Declare button variables
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
//Set audio save path
audioFilePath = getExternalFilesDir(null).getAbsolutePath() + "/audio_record.aac";
// Initialize MediaRecorder
mediaRecorder = new MediaRecorder();
mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC); // Use microphone as audio source
mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP); // Set audio file format
mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB); // Set audio encoding method
mediaRecorder.setOutputFile(audioFilePath); // Set output file path
// Step2: Set click event for start recording button
startButton.setOnClickListener(v -> {
// Call the method to start recording
Log.i("wtf","startRecording");
startRecording();
});

// Step3: Set click event for stop recording button
stopButton.setOnClickListener(v -> {
// Call the method to stop recording
Log.i("wtf","stopRecording");
stopRecording();
});
}
// Start recording
public void startRecording() {
try {
mediaRecorder.prepare(); // Prepare recording
mediaRecorder.start(); // Start recording
} catch (IOException e) {
System.out.print("Start has exception");
e.printStackTrace();
}
}

// Stop recording
public void stopRecording() {
try {
mediaRecorder.stop(); // Stop recording
mediaRecorder.release(); // Release resources
} catch (RuntimeException e) {
e.printStackTrace();
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (mediaRecorder != null) {
mediaRecorder.release(); // Ensure resource release
}
}
}

Step 3: Start and Stop Recording

You can control the start and stop of recording through buttons or other controls, for example:

Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);

startButton.setOnClickListener(v -> startRecording());
stopButton.setOnClickListener(v -> stopRecording());

Method 2: Low-Latency Audio Recording Using AudioRecord

If you need more precise audio control, AudioRecord is a lower-level interface suitable for real-time audio processing.

Step 1: Add Permissions

Also need to add recording permissions in the AndroidManifest.xml file:

<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.hardware.microphone" />

Step 2: Initialize AudioRecord

public class AudioRecorderActivity extends AppCompatActivity {
private static final int SAMPLE_RATE_IN_HZ = 44100; // Sample rate
private AudioRecord audioRecord;
private boolean isRecording = false;
private Thread recordingThread;
private String audioFilePath;
private FileOutputStream fileOutputStream; // File output stream

@Override
@RequiresPermission(Manifest.permission.RECORD_AUDIO)
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_audio_recorder);
// Declare button variables
Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);
// AudioRecord Initialization
int bufferSize = AudioRecord.getMinBufferSize(SAMPLE_RATE_IN_HZ, AudioFormat.CHANNEL_IN_MONO, AudioFormat.ENCODING_PCM_16BIT);

audioRecord = new AudioRecord(MediaRecorder.AudioSource.MIC,
SAMPLE_RATE_IN_HZ,
AudioFormat.CHANNEL_IN_MONO,
AudioFormat.ENCODING_PCM_16BIT,
bufferSize);

// Audio recording thread
recordingThread = new Thread(new Runnable() {
@Override
public void run() {
recordAudio();
}
});

startButton.setOnClickListener(v -> {
// Call the method to start recording
Log.i("wtf","startRecording");
startRecording();
});

// Step3: Set click event for stop recording button
stopButton.setOnClickListener(v -> {
// Call the method to stop recording
Log.i("wtf","stopRecording");
stopRecording();
});
}

// Start recording
public void startRecording() {
audioRecord.startRecording();
isRecording = true;
recordingThread.start();
}

// Stop recording
public void stopRecording() {
isRecording = false;
audioRecord.stop();
audioRecord.release();
}

private void recordAudio() {
// Create audio file path (in private directory of external storage)
String audioFileName = "audio_record_" + System.currentTimeMillis() + ".wav";
File audioFile = new File(getExternalFilesDir(null), audioFileName);
audioFilePath = audioFile.getAbsolutePath(); // Save file path to member variable

byte[] audioBuffer = new byte[1024];
int totalBytesRead = 0;

try (FileOutputStream fos = new FileOutputStream(audioFile)) {
// First write WAV file header (placeholder, fill with actual values later)
writeWavHeader(fos, 0, 0);

while (isRecording) {
int read = audioRecord.read(audioBuffer, 0, audioBuffer.length);
if (read > 0) {
// Write audio data to file
fos.write(audioBuffer, 0, read);
totalBytesRead += read;
}
}

// After recording is complete, update the actual data size in the WAV file header
updateWavHeader(audioFile, totalBytesRead);

} catch (IOException e) {
Log.e("AudioRecord", "File write failed: " + e.getMessage());
}
}

WAV File Header Related Code

// Write WAV file header (initial placeholder version)
private void writeWavHeader(FileOutputStream out, long totalAudioLen, long totalDataLen) throws IOException {
long sampleRate = SAMPLE_RATE_IN_HZ;
int channels = 1; // Mono
int bitsPerSample = 16; // 16-bit

byte[] header = new byte[44];

// RIFF/WAVE header
header[0] = 'R'; header[1] = 'I'; header[2] = 'F'; header[3] = 'F';
header[4] = (byte) (totalDataLen & 0xff);
header[5] = (byte) ((totalDataLen >> 8) & 0xff);
header[6] = (byte) ((totalDataLen >> 16) & 0xff);
header[7] = (byte) ((totalDataLen >> 24) & 0xff);

// WAVE
header[8] = 'W'; header[9] = 'A'; header[10] = 'V'; header[11] = 'E';

// 'fmt ' chunk
header[12] = 'f'; header[13] = 'm'; header[14] = 't'; header[15] = ' ';

// 4 bytes: size of 'fmt ' chunk
header[16] = 16; header[17] = 0; header[18] = 0; header[19] = 0;

// format = 1 (PCM)
header[20] = 1; header[21] = 0;

// number of channels
header[22] = (byte) channels; header[23] = 0;

// sample rate
header[24] = (byte) (sampleRate & 0xff);
header[25] = (byte) ((sampleRate >> 8) & 0xff);
header[26] = (byte) ((sampleRate >> 16) & 0xff);
header[27] = (byte) ((sampleRate >> 24) & 0xff);

// byte rate
long byteRate = sampleRate * channels * bitsPerSample / 8;
header[28] = (byte) (byteRate & 0xff);
header[29] = (byte) ((byteRate >> 8) & 0xff);
header[30] = (byte) ((byteRate >> 16) & 0xff);
header[31] = (byte) ((byteRate >> 24) & 0xff);

// block align
header[32] = (byte) (channels * bitsPerSample / 8);
header[33] = 0;

// bits per sample
header[34] = (byte) bitsPerSample;
header[35] = 0;

// data chunk
header[36] = 'd'; header[37] = 'a'; header[38] = 't'; header[39] = 'a';
header[40] = (byte) (totalAudioLen & 0xff);
header[41] = (byte) ((totalAudioLen >> 8) & 0xff);
header[42] = (byte) ((totalAudioLen >> 16) & 0xff);
header[43] = (byte) ((totalAudioLen >> 24) & 0xff);

out.write(header, 0, 44);
}

// Update the actual data size in the WAV file header
private void updateWavHeader(File file, int totalAudioLen) {
try (RandomAccessFile raf = new RandomAccessFile(file, "rw")) {
long totalDataLen = totalAudioLen + 36; // 36 = 44 - 8
raf.seek(4);
raf.write((int) (totalDataLen & 0xff));
raf.write((int) ((totalDataLen >> 8) & 0xff));
raf.write((int) ((totalDataLen >> 16) & 0xff));
raf.write((int) ((totalDataLen >> 24) & 0xff));

// Update audio data size
raf.seek(40);
raf.write((int) (totalAudioLen & 0xff));
raf.write((int) ((totalAudioLen >> 8) & 0xff));
raf.write((int) ((totalAudioLen >> 16) & 0xff));
raf.write((int) ((totalAudioLen >> 24) & 0xff));
} catch (IOException e) {
Log.e("AudioRecord", "Error updating WAV header: " + e.getMessage());
}
}

@Override
protected void onDestroy() {
super.onDestroy();
if (audioRecord != null) {
audioRecord.release(); // Ensure resource release
}
}

Step 3: Start and Stop Recording

Similarly, use buttons or other controls to control start and stop recording:

Button startButton = findViewById(R.id.startButton);
Button stopButton = findViewById(R.id.stopButton);

startButton.setOnClickListener(v -> startRecording());
stopButton.setOnClickListener(v -> stopRecording());

Method 3: Save Recording Data to File

If you want to save audio data to a file, you can save the audio stream to a file via AudioRecord's write method, or directly use MediaRecorder to save it as a .3gp format file.


Other Considerations

  1. Permission Issues: Same as the camera, it is recommended to enable microphone permission by default in system settings to avoid dynamic permission requests.
  2. Device Compatibility: Ensure the device supports microphone functionality, check AudioManager and MediaRecorder characteristics.
  3. Thread Management: If using AudioRecord, audio recording needs to be done in a background thread, otherwise it may cause ANR (Application Not Responding).
  4. Simple Recording Scenarios: Recommended to use MediaRecorder.
  5. Scenarios Requiring Real-time Control: Use AudioRecord.

🖥️ SPI Screen Control Method

Supports smile, starry eyes, speechless expressions, eyeball and blink control. Custom images can also be set.

Add the above aar dependency in Android gradle

//Place the .aar file in the module's libs/ directory
app/
├── libs/
│ └── libSpiScreen.aar
├── build.gradle
└── ...

//Add in the module's build.gradle:
dependencies {
implementation(files("./libs/libSpiScreen.aar"))
}

Control Eye Interface

// Open spi screen
EyeAction.openSpiEye()
//Methods to control eyes
EyeAction.blinkEye() // Blink once
EyeAction.blinkEye(240, 2) // Blink twice
EyeAction.updateEmotionSmile() // Smile
EyeAction.closeEye() // Close eyes
EyeAction.updateEmotionWuYu() // Speechless
EyeAction.updateEmotionStar() // Starry eyes

// Control eyeball position, x: -100-100, y: -100-100 center point 0 y positive down x positive right
// Control both eyeball positions simultaneously
EyeAction.updateBothScleraXY( e0.x, e0.y, e1.x, e1.y )
// Control eye position, 0 left eye 1 right eye
EyeAction.updateScleraXY(0, e0.x, e0.y)

// Close spi screen
EyeAction.closeSpiEye()

Custom Eye Image

Convert png image to RGB565 data

// Open two screens
val open0Rst = GC9A01Native.open(0) // Screen 0
val open1Rst = GC9A01Native.open(1) // Screen 1

// Draw image
val bitmapBytes = loadPngAsRGB565(this, R.raw.eye)
GC9A01Native.drawBitmap(
0,
0, 0, // x, y start position
240, 240, // width, height
bitmapBytes // image data
)

GC9A01Native.drawBitmap(
1,
0, 0, // x, y start position
240, 240, // width, height
bitmapBytes // image data
)

// Close two screens
GC9A01Native.close(0) // Screen 0
GC9A01Native.close(1) // Screen 1