Skip to content

Commit

Permalink
Merge pull request #9 from gifflet/feature/android-screen-capture-bro…
Browse files Browse the repository at this point in the history
…adcast

Replace OrientationEventListener with Broadcast Receiver for Screen Capture
  • Loading branch information
shamanec authored Feb 6, 2025
2 parents 01f8134 + 68ef314 commit 3147a9b
Show file tree
Hide file tree
Showing 8 changed files with 113 additions and 74 deletions.
5 changes: 3 additions & 2 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,10 @@ android {
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
namespace 'com.shamanec.stream'
}

dependencies {
Expand Down
4 changes: 3 additions & 1 deletion app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="com.shamanec.stream"
android:versionCode="1"
android:versionName="1.0">

Expand All @@ -11,7 +10,10 @@
<uses-permission android:name="android.permission.INJECT_EVENTS"
tools:ignore="ProtectedPermissions" />

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

<application
android:name=".GadsApp"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="GADS-Stream">
Expand Down
35 changes: 35 additions & 0 deletions app/src/main/java/com/shamanec/stream/GadsApp.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package com.shamanec.stream;

import android.app.Application;
import android.content.Intent;
import android.content.res.Configuration;
import android.util.Log;

import static com.shamanec.stream.IntentActionConstants.ORIENTATION_CHANGED_ACTION;

public class GadsApp extends Application {

private static final String TAG = "GadsApp";
private int lastKnownOrientation = Configuration.ORIENTATION_UNDEFINED;

@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
Log.d(TAG, "Configuration changed: " + newConfig.toString());

// Check if the orientation has actually changed
if (newConfig.orientation == lastKnownOrientation) {
Log.d(TAG, "Orientation did not change, skipping broadcast.");
return;
}

// Update the last known orientation
lastKnownOrientation = newConfig.orientation;

// Send the broadcast
Intent intent = new Intent(ORIENTATION_CHANGED_ACTION);
intent.setPackage(getPackageName());
sendBroadcast(intent);
Log.d(TAG, "Broadcast sent: " + ORIENTATION_CHANGED_ACTION);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.shamanec.stream;

public final class IntentActionConstants {

public static final String ORIENTATION_CHANGED_ACTION = "com.shamanec.stream.ORIENTATION_CHANGED";

private IntentActionConstants() {}

}
124 changes: 57 additions & 67 deletions app/src/main/java/com/shamanec/stream/ScreenCaptureService.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.shamanec.stream;

import static com.shamanec.stream.IntentActionConstants.ORIENTATION_CHANGED_ACTION;

import android.annotation.SuppressLint;
import android.app.Activity;
import android.app.Notification;
import android.app.Service;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Resources;
import android.content.IntentFilter;
import android.graphics.Bitmap;
import android.graphics.PixelFormat;
import android.hardware.display.DisplayManager;
Expand All @@ -15,29 +18,23 @@
import android.media.ImageReader;
import android.media.projection.MediaProjection;
import android.media.projection.MediaProjectionManager;
import android.os.Build;
import android.os.Handler;
import android.os.IBinder;
import android.os.Looper;
import android.util.DisplayMetrics;
import android.util.Log;
import android.view.Display;
import android.view.OrientationEventListener;
import android.view.WindowManager;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Objects;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;

import androidx.core.util.Pair;
import androidx.test.platform.app.InstrumentationRegistry;
import androidx.test.uiautomator.UiDevice;

public class ScreenCaptureService extends Service {
private MediaProjection mMediaProjection;
Expand All @@ -49,12 +46,13 @@ public class ScreenCaptureService extends Service {
private int mWidth;
private int mHeight;
private int mRotation;
private OrientationChangeCallback mOrientationChangeCallback;

private int targetFPS = 15;
private int jpegQuality = 90;
private int scalingFactor = 2;
private long frameIntervalMs = 1000 / targetFPS;
private BroadcastReceiver configurationChangeReceiver;
private static final String TAG = "ScreenCaptureService";

LocalWebsocketServer server;

Expand Down Expand Up @@ -165,25 +163,6 @@ public void run() {
}
}
}

private String getBitmapHash(Bitmap bitmap) {
try {
MessageDigest digest = MessageDigest.getInstance("MD5");
ByteBuffer buffer = ByteBuffer.allocate(bitmap.getByteCount());
bitmap.copyPixelsToBuffer(buffer);
byte[] pixelData = buffer.array();
byte[] hashBytes = digest.digest(pixelData);

// Convert hash bytes to hex string
StringBuilder hashString = new StringBuilder();
for (byte b : hashBytes) {
hashString.append(String.format("%02x", b));
}
return hashString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
}

private class ImageAvailableListener implements ImageReader.OnImageAvailableListener {
Expand All @@ -202,38 +181,6 @@ public void onImageAvailable(ImageReader reader) {
}
}

// Callback to reset the virtual display when orientation changes
private class OrientationChangeCallback extends OrientationEventListener {

OrientationChangeCallback(Context context) {
super(context);
}

@Override
public void onOrientationChanged(int orientation) {
imageQueue.clear();
// When orientation changes get the display rotation
final int rotation = mDisplay.getRotation();
// If the current rotation is different than the previous rotation
// Re-assign it
if (rotation != mRotation) {
mRotation = rotation;
try {
// Clean up the previous virtual display if it exists
if (mVirtualDisplay != null) mVirtualDisplay.release();

// Start the onImageAvailableListener on the image reader
if (mImageReader != null) mImageReader.setOnImageAvailableListener(null, null);

// re-create virtual display depending on device width / height
createVirtualDisplay();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}

// The MediaProjection.Callback class is a callback interface for receiving updates about changes to a media projection.
// this code ensures that resources used by the media projection are properly released when the media projection stops, which helps to avoid memory leaks and other issues.
private class MediaProjectionStopCallback extends MediaProjection.Callback {
Expand All @@ -244,7 +191,6 @@ public void onStop() {
// Release virtual display, image reader and orientation change callback
if (mVirtualDisplay != null) mVirtualDisplay.release();
if (mImageReader != null) mImageReader.setOnImageAvailableListener(null, null);
if (mOrientationChangeCallback != null) mOrientationChangeCallback.disable();
// Unregister the MediaProjectionStopCallback object (this) from the media projection.
// This ensures that the onStop() method is not called again if the media projection is stopped again in the future.
mMediaProjection.unregisterCallback(MediaProjectionStopCallback.this);
Expand All @@ -257,10 +203,54 @@ public IBinder onBind(Intent intent) {
return null;
}

@SuppressLint({"UnspecifiedRegisterReceiverFlag"})
@Override
public void onCreate() {
super.onCreate();

configurationChangeReceiver = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (ORIENTATION_CHANGED_ACTION.equals(intent.getAction())) {
imageQueue.clear();
// When orientation changes get the display rotation
final int rotation = mDisplay.getRotation();
// If the current rotation is different than the previous rotation
// Re-assign it
if (rotation != mRotation) {
mRotation = rotation;
try {
// Clean up the previous virtual display if it exists
if (mVirtualDisplay != null) mVirtualDisplay.release();

// Start the onImageAvailableListener on the image reader
if (mImageReader != null)
mImageReader.setOnImageAvailableListener(null, null);

// re-create virtual display depending on device width / height
createVirtualDisplay();
} catch (Exception e) {
e.printStackTrace();
}
}
}
}
};

IntentFilter orientationChangeFilter = new IntentFilter(ORIENTATION_CHANGED_ACTION);

try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // API 26+
registerReceiver(configurationChangeReceiver, orientationChangeFilter, Context.RECEIVER_NOT_EXPORTED);
Log.d(TAG, "Receiver registered with RECEIVER_NOT_EXPORTED (API 26+).");
} else { // API below 26
registerReceiver(configurationChangeReceiver, orientationChangeFilter);
Log.d(TAG, "Receiver registered without flags (API < 26).");
}
} catch (IllegalArgumentException e) {
Log.e(TAG, "Error registering receiver: " + e.getMessage());
}

// start capture handling thread
new Thread() {
@Override
Expand All @@ -272,6 +262,12 @@ public void run() {
}.start();
}

@Override
public void onDestroy() {
unregisterReceiver(configurationChangeReceiver);
super.onDestroy();
}

// The onStartCommand() method is called by the Android system when the service is started
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
Expand Down Expand Up @@ -311,12 +307,6 @@ private void startProjection(int resultCode, Intent data) {
// Get the actual display
mDisplay = windowManager.getDefaultDisplay();
createVirtualDisplay();

// Register orientation change callback
mOrientationChangeCallback = new OrientationChangeCallback(this);
if (mOrientationChangeCallback.canDetectOrientation()) {
mOrientationChangeCallback.enable();
}
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
// Top-level build file where you can add configuration options common to all sub-projects/modules.
plugins {
id 'com.android.application' version '7.3.1' apply false
id 'com.android.library' version '7.3.1' apply false
id 'com.android.application' version '8.8.0' apply false
id 'com.android.library' version '8.8.0' apply false
}
4 changes: 3 additions & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,6 @@ android.useAndroidX=true
# Enables namespacing of each library's R class so that its R class includes only the
# resources declared in the library itself and none from the library's dependencies,
# thereby reducing the size of the R class for that library
android.nonTransitiveRClass=true
android.nonTransitiveRClass=true
android.defaults.buildfeatures.buildconfig=true
android.nonFinalResIds=false
2 changes: 1 addition & 1 deletion gradle/wrapper/gradle-wrapper.properties
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#Thu Mar 02 10:05:18 EET 2023
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME

0 comments on commit 3147a9b

Please sign in to comment.