TaterLi 个人博客

[Android速通]联网通信和多媒体

到这里终于开始主要接触别人的轮子,其实到这里已经有能力做一些简单App,不过一些常用的轮子还是要抽出来学一下,当然里面也会贯穿一些常用知识点.

下一步,看一下如何让App联网通信,在学习网络应用之前,要先明确一点,只有UI线程可以刷屏,同时,网络很耗时,所以不能放在UI线程,所以这就有前后分离,比如下面这个例子.

private class PlayThread extends Thread {
    @Override
    public void run(){
        while(true){
            Calendar calendar = Calendar.getInstance();
            int hour = calendar.get(Calendar.HOUR_OF_DAY);
            int minute = calendar.get(Calendar.MINUTE);
            int second = calendar.get(Calendar.SECOND);

            String time = String.format("%02d:%02d:%02d", hour, minute, second);

            // mHandler.sendEmptyMessage(0);
            Message message = Message.obtain();
            message.what = SET_TXT;
            message.obj = time;
            mHandler.sendMessage(message);
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 创建处理器对象mHandler
private Handler mHandler = new Handler(Looper.getMainLooper()){
    public void handleMessage(Message message){
        // 收到Msg
        if (message.what == SET_TXT){
            tv_hello.setText(message.obj.toString());
        }
    }
};

当然因为他很简单,他也可以用runOnUiThread直接在内部往外调用.

private class PlayThread extends Thread {
    @Override
    public void run(){
        while(true){
            Calendar calendar = Calendar.getInstance();
            int hour = calendar.get(Calendar.HOUR_OF_DAY);
            int minute = calendar.get(Calendar.MINUTE);
            int second = calendar.get(Calendar.SECOND);

            String time = String.format("%02d:%02d:%02d", hour, minute, second);

            runOnUiThread(() -> {
                tv_hello.setText(time);
            });
         
            try {
                sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

除了这种方法,安卓还提供了WorkManager,需要修改build.gradle引入.

dependencies {

    implementation libs.appcompat
    implementation libs.material
    implementation libs.activity
    implementation libs.constraintlayout
    testImplementation libs.junit
    androidTestImplementation libs.ext.junit
    androidTestImplementation libs.espresso.core
    
    // WorkManager库各版本见 https://mvnrepository.com/artifact/androidx.work/work-runtime
    implementation 'androidx.work:work-runtime:2.10.0'
}

使用WordManager可以把复杂的任务独立成一个类里,并由系统安排他合适的时间执行,异步获得数据,当然,任务不是UI线程,他依然不可以刷新标签.

先创建一个包含生成随机数能力的Worker.

public class RandomNumberWorker extends Worker {
    public static final String KEY_MIN_VALUE = "min_value";
    public static final String KEY_MAX_VALUE = "max_value";
    public static final String KEY_RESULT = "random_result";

    public RandomNumberWorker(
            @NonNull Context context,
            @NonNull WorkerParameters workerParams) {
        super(context, workerParams);
    }

    @NonNull
    @Override
    public Result doWork() {
        // 获取输入数据
        int min = getInputData().getInt(KEY_MIN_VALUE, 0);
        int max = getInputData().getInt(KEY_MAX_VALUE, 100);

        // 生成随机数
        Random random = new Random();
        int randomNumber = random.nextInt(max - min + 1) + min;

        // 创建输出数据
        Data outputData = new Data.Builder()
                .putInt(KEY_RESULT, randomNumber)
                .build();

        // 返回结果
        return Result.success(outputData);
    }
}

之后在其他线程(比如UI线程)安排这个任务,并在可用时获得他的返回.

// 创建输入数据
Data inputData = new Data.Builder()
        .putInt(RandomNumberWorker.KEY_MIN_VALUE, 10)
        .putInt(RandomNumberWorker.KEY_MAX_VALUE, 50)
        .build();

// 创建约束条件
Constraints constraints = new Constraints.Builder()
        .setRequiredNetworkType(NetworkType.CONNECTED)
        .setRequiresBatteryNotLow(true)
        .build();

// 创建一次性工作请求
OneTimeWorkRequest workRequest = new OneTimeWorkRequest.Builder(RandomNumberWorker.class)
        .setInputData(inputData)
        .setConstraints(constraints)
        .build();

// 观察工作状态
WorkManager.getInstance(this).getWorkInfoByIdLiveData(workRequest.getId())
        .observe(this, new Observer<WorkInfo>() {
            @Override
            public void onChanged(WorkInfo workInfo) {
                if (workInfo != null) {
                    if (workInfo.getState() == WorkInfo.State.SUCCEEDED) {
                        // 任务完成,获取输出数据
                        int result = workInfo.getOutputData().getInt(
                                RandomNumberWorker.KEY_RESULT, 0);
                        tvHello.setText("随机数: " + result);
                    } else if (workInfo.getState() == WorkInfo.State.FAILED) {
                        tvHello.setText("任务失败");
                    }
                }
            }
        });

// 启动作业
WorkManager.getInstance(this).enqueue(workRequest);
tvHello.setText("任务已启动...");

除了一次性任务外,也可以创建周期性任务.

// 创建周期性工作请求 (最小间隔15分钟)
PeriodicWorkRequest periodicWorkRequest = 
        new PeriodicWorkRequest.Builder(RandomNumberWorker.class, 
                15, TimeUnit.MINUTES)
                .setInputData(new Data.Builder()
                        .putInt(RandomNumberWorker.KEY_MIN_VALUE, 100)
                        .putInt(RandomNumberWorker.KEY_MAX_VALUE, 200)
                        .build())
                .build();

下面说网络通信,最常见就是调用各种API,大多数API返回都是JSON,所以也先引入一个JSON库.

// gson库各版本见 https://mvnrepository.com/artifact/com.google.code.gson/gson
implementation 'com.google.code.gson:gson:2.13.0'
// okhttp库各版本见 https://mvnrepository.com/artifact/com.squareup.okhttp3/okhttp
implementation 'com.squareup.okhttp3:okhttp:4.12.0'

为了上网,还得添加权限.

<uses-permission android:name="android.permission.INTERNET" />

一般情况下,比如我写了一个简单的图片上传,并通过返回的来判断是否成功.

private boolean uploadImage(String imageUri, String serverFileName) {
    OkHttpClient client = new OkHttpClient();
    File file = new File(imageUri);
    Gson gson = new Gson();

    if (!file.exists()) {
        return false;
    }

    // 如果未指定服务器文件名,则使用原文件名
    String uploadFileName = (serverFileName == null || serverFileName.isEmpty())
            ? file.getName() : serverFileName;

    RequestBody requestBody = new MultipartBody.Builder()
            .setType(MultipartBody.FORM)
            .addFormDataPart("filename",uploadFileName)
            .addFormDataPart("file", uploadFileName,
                    RequestBody.create(MediaType.parse("image/*"), file))
            .build();

    Request request = new Request.Builder()
            .url("https://note.242345.xyz/DCIM/index.php") // 替换为你的上传URL
            .post(requestBody)
            .build();

    try (Response response = client.newCall(request).execute()) {
        if (!response.isSuccessful()) {
            return false;
        }

        // 解析服务器返回的JSON
        String responseBody = response.body().string();
        ServerResponse serverResponse = gson.fromJson(responseBody, ServerResponse.class);

        // 根据服务器返回的success字段判断是否成功
        if (serverResponse != null && serverResponse.success){
            AppDatabase database = AppDatabase.getDatabase(getApplicationContext());
            ImageProcessor processor = new ImageProcessor(getApplicationContext());
            processor.processImage(imageUri, database.imageDao());
            return true;
        }else{
            return false;
        }
    } catch (IOException e) {
        Log.e(TAG, "上传图片失败", e);
        return false;
    }
}

我通过运用之前数据库的知识,简单拼了一个自动上传备份照片的功能,当然毕竟是一个学了几天的新手,代码肯定很烂.

https://github.com/nickfox-taterli/simple-android-leran

主题逻辑就是打开后会自动上传图片,每次上传后会刷新视图,显示剩余还没上传的图片,再次说了,是非常烂的代码.

除了这些HTTP请求外,还有Socket和WebSocket两种,因为HTTP是单向查询的,所以对于某些需要推送的就不太好用,因为安卓已经发展很多年了,太多轮子可以用了,但是服务器端的话也要采用一些兼容SocketIO库的选择才行,不然的话直接实现看了下还是挺复杂的.

除了联网通信外,App中还有很多常见的应用,比如播放,录制音频,播放,录制视频,拍照,显示图片.

启动系统自带录音机进行录音.

private void startSystemRecording() {
    // 检查权限
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) 
            != PackageManager.PERMISSION_GRANTED) {
        ActivityCompat.requestPermissions(this, permissions, REQUEST_RECORD_AUDIO_PERMISSION);
        return;
    }
    
    // 启动系统录音应用
    Intent intent = new Intent(MediaStore.Audio.Media.RECORD_SOUND_ACTION);
    if (intent.resolveActivity(getPackageManager()) != null) {
        startActivityForResult(intent, REQUEST_RECORD_AUDIO_PERMISSION);
    } else {
        Toast.makeText(this, "No recording app found", Toast.LENGTH_SHORT).show();
    }
}

@Override
protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
    super.onActivityResult(requestCode, resultCode, data);
    if (requestCode == REQUEST_RECORD_AUDIO_PERMISSION && resultCode == RESULT_OK && data != null) {
        Uri audioUri = data.getData();
        // 处理录音文件
        if (audioUri != null) {
            Toast.makeText(this, "Recording saved: " + audioUri.toString(), Toast.LENGTH_SHORT).show();
        }
    }
}

大多数App其实是都是内部支持录音,比如微信就不可能跳转到外面录音啊.

private void checkAndStartRecording() {
    if (ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO) 
            != PackageManager.PERMISSION_GRANTED || 
        ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) 
            != PackageManager.PERMISSION_GRANTED) {
        
        ActivityCompat.requestPermissions(this, 
            new String[]{
                Manifest.permission.RECORD_AUDIO,
                Manifest.permission.WRITE_EXTERNAL_STORAGE
            }, 
            REQUEST_PERMISSION_CODE);
    } else {
        startRecording();
    }
}

private void startRecording() {
    try {
        // 创建输出文件
        outputFile = getExternalCacheDir().getAbsolutePath() + "/recording.3gp";
        
        mediaRecorder = new MediaRecorder();
        mediaRecorder.setAudioSource(MediaRecorder.AudioSource.MIC);
        mediaRecorder.setOutputFormat(MediaRecorder.OutputFormat.THREE_GPP);
        mediaRecorder.setOutputFile(outputFile);
        mediaRecorder.setAudioEncoder(MediaRecorder.AudioEncoder.AMR_NB);
        
        mediaRecorder.prepare();
        mediaRecorder.start();
        isRecording = true;
        Toast.makeText(this, "Recording started", Toast.LENGTH_SHORT).show();
    } catch (IOException e) {
        Log.e("MediaRecorder", "Recording failed", e);
        releaseMediaRecorder();
    }
}

private void stopRecording() {
    if (isRecording) {
        mediaRecorder.stop();
        isRecording = false;
        Toast.makeText(this, "Recording saved to " + outputFile, Toast.LENGTH_LONG).show();
    }
    releaseMediaRecorder();
}

private void releaseMediaRecorder() {
    if (mediaRecorder != null) {
        mediaRecorder.release();
        mediaRecorder = null;
    }
}

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults);
    if (requestCode == REQUEST_PERMISSION_CODE) {
        if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
            startRecording();
        } else {
            Toast.makeText(this, "Permission denied", Toast.LENGTH_SHORT).show();
        }
    }
}

@Override
protected void onStop() {
    super.onStop();
    releaseMediaRecorder();
}

播放音乐也是同理的.

private void playAudioWithMediaPlayer(String audioPath) {
    try {
        if (mediaPlayer == null) {
            mediaPlayer = new MediaPlayer();
        } else {
            mediaPlayer.reset();
        }
        
        mediaPlayer.setDataSource(audioPath); // 可以是本地文件路径或网络URL
        mediaPlayer.prepareAsync(); // 异步准备
        
        mediaPlayer.setOnPreparedListener(mp -> {
            mp.start(); // 准备好后开始播放
            Log.d("MediaPlayer", "Duration: " + mp.getDuration() + "ms");
        });
        
        mediaPlayer.setOnCompletionListener(mp -> {
            releaseMediaPlayer();
            Log.d("MediaPlayer", "Playback completed");
        });
        
        mediaPlayer.setOnErrorListener((mp, what, extra) -> {
            releaseMediaPlayer();
            Log.e("MediaPlayer", "Playback error: " + what + ", " + extra);
            return true;
        });
    } catch (IOException e) {
        Log.e("MediaPlayer", "Playback failed", e);
        releaseMediaPlayer();
    }
}

private void releaseMediaPlayer() {
    if (mediaPlayer != null) {
        mediaPlayer.release();
        mediaPlayer = null;
    }
}

// 暂停播放
private void pausePlayback() {
    if (mediaPlayer != null && mediaPlayer.isPlaying()) {
        mediaPlayer.pause();
    }
}

// 继续播放
private void resumePlayback() {
    if (mediaPlayer != null && !mediaPlayer.isPlaying()) {
        mediaPlayer.start();
    }
}

@Override
protected void onDestroy() {
    super.onDestroy();
    releaseMediaPlayer();
}

使用MediaStore.ACTION_VIDEO_CAPTURE意图可以调起系统的拍照和录像,这里也不展开了,但是用CamerX可以在软件内部直接用相机,包括拍照和录像.

private void takePhoto() {
    // 创建输出文件
    File photoFile = new File(
        getOutputDirectory(),
        new SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS").format(new Date()) + ".jpg"
    );
    
    ImageCapture.OutputFileOptions outputFileOptions =
        new ImageCapture.OutputFileOptions.Builder(photoFile).build();
    
    imageCapture.takePicture(
        outputFileOptions,
        ContextCompat.getMainExecutor(this),
        new ImageCapture.OnImageSavedCallback() {
            @Override
            public void onImageSaved(@NonNull ImageCapture.OutputFileResults outputFileResults) {
                String savedUri = photoFile.getAbsolutePath();
                Toast.makeText(MainActivity.this, "Photo saved: " + savedUri, Toast.LENGTH_SHORT).show();
            }
            
            @Override
            public void onError(@NonNull ImageCaptureException exception) {
                Log.e("CameraX", "Photo capture failed: " + exception.getMessage(), exception);
            }
        }
    );
}

之前说的播放都要求资源在本地,还有一个轮子叫ExoPlayer就支持网络流,各种动态加载都很方便.这一次学习很多已经是调包的过程了,虽然看起来技术含量不高,但是需要反复实践.

退出移动版