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