From 27bd52d4159e5e243870627e576aee661f1bdc04 Mon Sep 17 00:00:00 2001 From: Jerry Date: Thu, 17 Aug 2023 22:56:02 +0800 Subject: [PATCH] update --- app/build.gradle | 4 +- app/src/main/AndroidManifest.xml | 5 +- .../com/jerryxiao/droidcast/MainActivity.java | 137 +++++++++++----- .../jerryxiao/droidcast/ScreenCapService.java | 147 +++++++++++------- .../jerryxiao/droidcast/SettingsActivity.java | 2 +- app/src/main/res/layout/main_activity.xml | 68 ++++---- app/src/main/res/values-zh-rCN/strings.xml | 10 +- app/src/main/res/values/arrays.xml | 2 + app/src/main/res/values/strings.xml | 16 +- 9 files changed, 258 insertions(+), 133 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4912ce4..cb82de0 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -23,8 +23,8 @@ android { } } compileOptions { - sourceCompatibility JavaVersion.VERSION_1_8 - targetCompatibility JavaVersion.VERSION_1_8 + sourceCompatibility JavaVersion.VERSION_17 + targetCompatibility JavaVersion.VERSION_17 } } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index bdf0a70..fed3ef8 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,9 +26,11 @@ + @@ -39,7 +41,6 @@ screenCapReq; private ScreenCapService screenCapService; - private Button startButton; - private TextView infoDisplay; private SharedPreferences prefs; private final ServiceConnection serviceConnection = new ServiceConnection() { - public void onServiceConnected(ComponentName componentName, IBinder binder) { - MainActivity.this.screenCapService = ((ScreenCapService.MBinder)binder).getService(); - MainActivity.this.screenCapDisplay = MainActivity.this.screenCapService.getScreenCapDisplay(); - MainActivity.this.screenCapReq.launch(MainActivity.this.screenCapDisplay.sendIntent()); - MainActivity.this.screenCapService.addDisconnectCallback(() -> { - unbindService(serviceConnection); - runOnUiThread(() -> { - startButton.setText(getString(R.string.start_button)); - }); - MainActivity.this.screenCapService = null; - }); - MainActivity.this.screenCapService.addInfoDisplayCallback((t) -> { - runOnUiThread(() -> { - MainActivity.this.infoDisplay.setText(t); - }); - }); + private void updateButtonText(boolean connected) { + Log.d(TAG, String.format("updateButtonText: %b", connected)); runOnUiThread(() -> { - MainActivity.this.startButton.setText(getString(R.string.stop_button)); + ((Button)MainActivity.this.findViewById(R.id.startButton)).setText(connected + ? getString(R.string.stop_button) + : getString(R.string.start_button)); }); } + private void updateInfoDisplay(String t) { + Log.d(TAG, String.format("updateInfoDisplay: %s", t)); + runOnUiThread(() -> { + ((TextView)MainActivity.this.findViewById(R.id.infoDisplay)).setText(t); + }); + } + public void onServiceConnected(ComponentName componentName, IBinder binder) { + Log.d(TAG, "service connected"); + MainActivity.this.screenCapService = ((ScreenCapService.MBinder)binder).getService(); + MainActivity.this.screenCapService.addConnectionStateCallback(this::updateButtonText); + MainActivity.this.screenCapService.addInfoDisplayCallback(this::updateInfoDisplay); + this.updateButtonText(MainActivity.this.screenCapService != null && MainActivity.this.screenCapService.isConnected()); + this.updateInfoDisplay(MainActivity.this.screenCapService.getInfoDisplayText()); + } public void onServiceDisconnected(ComponentName componentName) { + Log.d(TAG, "service disconnected"); + MainActivity.this.screenCapService.removeConnectionStateCallback(this::updateButtonText); + MainActivity.this.screenCapService.removeInfoDisplayCallback(this::updateInfoDisplay); } }; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); + Log.d(TAG, "activity created"); this.prefs = PreferenceManager.getDefaultSharedPreferences(this); setContentView(R.layout.main_activity); + Map permissionsToRequest = Collections.emptyMap(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { + permissionsToRequest = new HashMap<>(Map.of("android.permission.POST_NOTIFICATIONS", + getString(R.string.permission_denied_post_notifications))); if (checkSelfPermission("android.permission.POST_NOTIFICATIONS") != PackageManager.PERMISSION_GRANTED) { + requestPermissions(new String[]{"android.permission.POST_NOTIFICATIONS"}, 0); } } - else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.R) { - if (checkSelfPermission("android.permission.RECORD_AUDIO") != PackageManager.PERMISSION_GRANTED) { - requestPermissions(new String[]{"android.permission.RECORD_AUDIO"}, 0); - } + else if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) { + permissionsToRequest = new HashMap<>(Map.of("android.permission.RECORD_AUDIO", + getString(R.string.permission_denied_record_audio))); + } + permissionsToRequest.entrySet().removeIf(i -> checkSelfPermission(i.getKey()) == PackageManager.PERMISSION_GRANTED); + if (!permissionsToRequest.isEmpty()) { + Log.d(TAG, String.format("requesting permissions %s", String.join(" + ", permissionsToRequest.keySet().toArray(new String[0])))); + Map finalPermissionsToRequest = permissionsToRequest; + ActivityResultLauncher permissionReq = registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), (result) -> { + result.forEach((perm, granted) -> { + if (!granted) { + Log.d(TAG, String.format("permission denied: %s", perm)); + (new AlertDialog.Builder(this)) + .setTitle(getText(R.string.error_title)) + .setMessage(finalPermissionsToRequest.get(perm)) + .setCancelable(false) + .setPositiveButton(getString(R.string.button_ok), (dialog, id) -> {}) + .create() + .show(); + } + }); + }); + permissionReq.launch(permissionsToRequest.keySet().toArray(new String[0])); } - this.startButton = (Button)this.findViewById(R.id.startButton); - this.infoDisplay = (TextView)this.findViewById(R.id.infoDisplay); this.screenCapReq = registerForActivityResult(new ActivityResultContracts.StartActivityForResult(), (activityResult) -> { int result = activityResult.getResultCode(); - screenCapDisplay.setIntentResult(result, activityResult.getData()); + screenCapService.getScreenCapDisplay().setIntentResult(result, activityResult.getData()); if (result == RESULT_OK) { + Log.d(TAG, "capture granted"); screenCapService.startCapture(prefs.getString("server", "")); } else { runOnUiThread(() -> { - Toast.makeText(MainActivity.this, getText(R.string.error_capture_denied), Toast.LENGTH_SHORT).show(); + Log.d(TAG, "capture denied"); + (new AlertDialog.Builder(this)) + .setTitle(getText(R.string.error_title)) + .setMessage(getText(R.string.error_capture_denied)) + .setCancelable(false) + .setPositiveButton(getString(R.string.button_ok), (dialog, id) -> {}) + .create() + .show(); }); - screenCapService.stopCapture(); } }); - this.startButton.setOnClickListener((view) -> { + bindService(new Intent(this, ScreenCapService.class), serviceConnection, BIND_AUTO_CREATE); + ((Button)this.findViewById(R.id.startButton)).setOnClickListener((view) -> { if (screenCapService != null) { - new Thread(() -> { - screenCapService.stopCapture(); - }).start(); + if (screenCapService.isConnected()) { + Log.d(TAG, "button stop"); + new Thread(() -> { if (screenCapService.getScreenCapDisplay() != null) screenCapService.getScreenCapDisplay().stopStream(); }).start(); + } + else { + Log.d(TAG, "button start"); + MainActivity.this.screenCapService.createDisplay(); + MainActivity.this.screenCapReq.launch(MainActivity.this.screenCapService.getScreenCapDisplay().sendIntent()); + } } else { - Intent intent = new Intent(this, ScreenCapService.class); - bindService(intent, serviceConnection, BIND_AUTO_CREATE); + Log.e(TAG, "service died"); + (new AlertDialog.Builder(this)) + .setTitle(getText(R.string.error_title)) + .setMessage(getText(R.string.error_service_died)) + .setCancelable(false) + .setPositiveButton(getString(R.string.button_ok), (dialog, id) -> {}) + .create() + .show(); } }); - Button settingsButton = (Button)this.findViewById(R.id.settingsButton); - settingsButton.setOnClickListener((view) -> { - Intent intent = new Intent(this, SettingsActivity.class); - startActivity(intent); + ((Button)this.findViewById(R.id.settingsButton)).setOnClickListener((view) -> { + startActivity(new Intent(this, SettingsActivity.class)); }); } + + @Override + protected void onDestroy() { + super.onDestroy(); + Log.d(TAG, "activity destroyed"); + unbindService(serviceConnection); + this.screenCapService = null; + } } diff --git a/app/src/main/java/com/jerryxiao/droidcast/ScreenCapService.java b/app/src/main/java/com/jerryxiao/droidcast/ScreenCapService.java index 650c988..7d07f01 100644 --- a/app/src/main/java/com/jerryxiao/droidcast/ScreenCapService.java +++ b/app/src/main/java/com/jerryxiao/droidcast/ScreenCapService.java @@ -15,6 +15,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.util.Log; import android.view.WindowManager; import android.widget.Toast; @@ -26,14 +27,21 @@ import com.pedro.encoder.utils.CodecUtil; import com.pedro.rtmp.utils.ConnectCheckerRtmp; import com.pedro.rtplibrary.rtmp.RtmpDisplay; -import java.util.LinkedList; +import java.util.HashSet; import java.util.function.Consumer; public class ScreenCapService extends Service { + private static final String TAG = "ScreenCapService"; private static final int RESOLUTION_GAP = 100000; - private final LinkedList disconnectCallbacks = new LinkedList<>(); - private final LinkedList> infoDisplayCallbacks = new LinkedList<>(); + private final HashSet> connectionStateCallbacks = new HashSet<>(); + private final HashSet> infoDisplayCallbacks = new HashSet<>(); private SharedPreferences prefs; + + private boolean connected = false; + public boolean isConnected() { + return connected; + } + protected class MBinder extends Binder { public ScreenCapService getService() { return ScreenCapService.this; @@ -46,6 +54,10 @@ public class ScreenCapService extends Service { public RtmpDisplay getScreenCapDisplay() { return this.screenCapDisplay; } + private String infoDisplayText = ""; + public String getInfoDisplayText() { + return infoDisplayText; + } public IBinder onBind(Intent intent) { return this.mBinder; @@ -53,70 +65,66 @@ public class ScreenCapService extends Service { public void onCreate() { super.onCreate(); + Log.d(TAG, "service created"); this.prefs = PreferenceManager.getDefaultSharedPreferences(this); - this.screenCapDisplay = new RtmpDisplay(this, prefs.getBoolean("use_gl", true), new ConnectCheckerRtmp() { + this.infoDisplayText = getString(R.string.status_disconnected); + } + + public void onDestroy() { + super.onDestroy(); + Log.d(TAG, "service destroyed"); + } + + protected void createDisplay() { + boolean use_gl = prefs.getBoolean("use_gl", true); + Log.d(TAG, String.format("creating display use_gl=%b", use_gl)); + this.screenCapDisplay = new RtmpDisplay(this, use_gl, new ConnectCheckerRtmp() { public void onAuthErrorRtmp() { String text = getString(R.string.status_auth_error); - (new Handler(Looper.getMainLooper())).post(() -> { - Toast.makeText(ScreenCapService.this, text, Toast.LENGTH_SHORT).show(); - }); - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(text); - } + showToast(text); + setInfoDisplay(text); ScreenCapService.this.stop(); } public void onAuthSuccessRtmp() { - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(getString(R.string.status_auth_success)); - } + setInfoDisplay(getString(R.string.status_auth_success)); } public void onConnectionFailedRtmp(@NonNull String reason) { String text = getString(R.string.status_connection_failed_format, reason); - (new Handler(Looper.getMainLooper())).post(() -> { - Toast.makeText(ScreenCapService.this, text, Toast.LENGTH_SHORT).show(); - }); - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(text); - } + showToast(text); + setInfoDisplay(text); ScreenCapService.this.stop(); } public void onConnectionStartedRtmp(@NonNull String rtmpUrl) { - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(getString(R.string.status_connecting)); - } + setInfoDisplay(getString(R.string.status_connecting, rtmpUrl)); + ScreenCapService.this.connectionStateChange(true); } public void onConnectionSuccessRtmp() { - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(getString(R.string.status_connected)); - } + setInfoDisplay(getString(R.string.status_connected)); } public void onDisconnectRtmp() { - String text = getString(R.string.status_disconnected); - (new Handler(Looper.getMainLooper())).post(() -> { - Toast.makeText(ScreenCapService.this, text, Toast.LENGTH_SHORT).show(); - }); - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(text); - } + setInfoDisplay(getString(R.string.status_disconnected)); ScreenCapService.this.stop(); } public void onNewBitrateRtmp(long bitrate) { - synchronized (ScreenCapService.this.infoDisplayCallbacks) { - for (Consumer stringConsumer : ScreenCapService.this.infoDisplayCallbacks) { - stringConsumer.accept(getString(R.string.status_bitrate_format, bitrate)); - } - } + setInfoDisplay(getString(R.string.status_bitrate_format, bitrate)); } }); } protected void startCapture(String server) { + if (this.connected) { + Log.d(TAG, "not starting capture"); + return; + } + Log.d(TAG, "start capture"); + startService(new Intent(this, ScreenCapService.class)); + this.connected = true; NotificationChannel notificationChannel = new NotificationChannel("notifications", "Notifications", NotificationManager.IMPORTANCE_LOW); notificationChannel.setDescription("Notifications"); getSystemService(NotificationManager.class).createNotificationChannel(notificationChannel); @@ -129,7 +137,9 @@ public class ScreenCapService extends Service { screenCapDisplay.setForce(prefs.getBoolean("software_video_encoder", false) ? CodecUtil.Force.SOFTWARE : CodecUtil.Force.FIRST_COMPATIBLE_FOUND, prefs.getBoolean("software_audio_encoder", false) ? CodecUtil.Force.SOFTWARE : CodecUtil.Force.FIRST_COMPATIBLE_FOUND); screenCapDisplay.setWriteChunkSize(prefs.getInt("write_chunk_size_int", 1024)); + screenCapDisplay.setReTries(prefs.getInt("retries_int", 0)); screenCapDisplay.resizeCache(prefs.getInt("cache_size_int", 120)); + screenCapDisplay.setLogs(prefs.getBoolean("enable_droidcast_logs", false)); if (prefs.getBoolean("use_gl", true) && prefs.getBoolean("use_force_render", true)) { screenCapDisplay.getGlInterface().setForceRender(true); } @@ -150,34 +160,42 @@ public class ScreenCapService extends Service { video_height = video_width; video_width = tmp; } + Log.d(TAG, String.format("capture width %d height %d", video_width, video_height)); if (this.screenCapDisplay.prepareInternalAudio(Integer.parseInt(prefs.getString("audio_bitrate", "131072")), 48000, prefs.getBoolean("stereo_audio", true), false, false) && this.screenCapDisplay.prepareVideo(video_width, video_height, Integer.parseInt(prefs.getString("fps", "30")), Integer.parseInt(prefs.getString("video_bitrate", "10485760")), 0, 320, -1, -1, 2)) { this.screenCapDisplay.startStream(server); } else { - (new Handler(Looper.getMainLooper())).post(() -> { - Toast.makeText(ScreenCapService.this, "Cannot prepare audio and video", Toast.LENGTH_SHORT).show(); - }); + Log.d(TAG, "cannot prepare video audio"); + String text = getString(R.string.error_prepare_video_audio); + setInfoDisplay(text); + showToast(text); this.stop(); } } private void stop() { + Log.d(TAG, "stop called"); + this.connected = false; this.stopForeground(true); this.stopSelf(); - synchronized (this.disconnectCallbacks) { - while (!this.disconnectCallbacks.isEmpty()) { - this.disconnectCallbacks.pop().run(); + this.connectionStateChange(false); + } + + private void connectionStateChange(boolean connected) { + synchronized (this.connectionStateCallbacks) { + for (Consumer booleanConsumer : this.connectionStateCallbacks) { + try { + booleanConsumer.accept(connected); + } + catch (Throwable ignored) {} } } - synchronized (this.infoDisplayCallbacks) { - this.infoDisplayCallbacks.clear(); - } } - protected void addDisconnectCallback(Runnable runnable) { - synchronized (this.disconnectCallbacks) { - this.disconnectCallbacks.add(runnable); + protected void addConnectionStateCallback(Consumer runnable) { + synchronized (this.connectionStateCallbacks) { + this.connectionStateCallbacks.add(runnable); } } @@ -187,8 +205,33 @@ public class ScreenCapService extends Service { } } - protected void stopCapture() { - this.screenCapDisplay.stopStream(); - this.stop(); + protected void removeConnectionStateCallback(Consumer runnable) { + synchronized (this.connectionStateCallbacks) { + this.connectionStateCallbacks.remove(runnable); + } + } + + protected void removeInfoDisplayCallback(Consumer stringConsumer) { + synchronized (this.infoDisplayCallbacks) { + this.infoDisplayCallbacks.remove(stringConsumer); + } + } + + private void setInfoDisplay(String t) { + synchronized (this.infoDisplayCallbacks) { + this.infoDisplayText = t; + for (Consumer stringConsumer : this.infoDisplayCallbacks) { + try { + stringConsumer.accept(t); + } + catch (Throwable ignored) {} + } + } + } + + private void showToast(String text) { + (new Handler(Looper.getMainLooper())).post(() -> { + Toast.makeText(ScreenCapService.this, text, Toast.LENGTH_SHORT).show(); + }); } } diff --git a/app/src/main/java/com/jerryxiao/droidcast/SettingsActivity.java b/app/src/main/java/com/jerryxiao/droidcast/SettingsActivity.java index 5aa7ba2..d5a17f8 100644 --- a/app/src/main/java/com/jerryxiao/droidcast/SettingsActivity.java +++ b/app/src/main/java/com/jerryxiao/droidcast/SettingsActivity.java @@ -20,7 +20,7 @@ public class SettingsActivity extends AppCompatActivity { } ActionBar actionBar = getSupportActionBar(); if (actionBar != null) { - actionBar.setDisplayHomeAsUpEnabled(false); + actionBar.setDisplayHomeAsUpEnabled(true); } } diff --git a/app/src/main/res/layout/main_activity.xml b/app/src/main/res/layout/main_activity.xml index e41b15e..ae04844 100644 --- a/app/src/main/res/layout/main_activity.xml +++ b/app/src/main/res/layout/main_activity.xml @@ -6,38 +6,50 @@ android:layout_height="match_parent" tools:context=".MainActivity"> -