From 32a606b9d6561855ffc5a06fa4465d224825685f Mon Sep 17 00:00:00 2001 From: Dmitriy Krivoruchko Date: Tue, 2 May 2017 12:27:02 +0300 Subject: [PATCH] ImageGenerator update --- app/build.gradle | 22 ++- app/proguard-rules.pro | 4 + .../screenstream/ScreenStreamApplication.java | 48 ++++++ .../screenstream/data/ImageGenerator.java | 142 +++++++++--------- .../service/ForegroundService.java | 18 +-- .../service/ForegroundServiceHandler.java | 27 ++-- .../dvkr/screenstream/view/MainActivity.java | 2 + build.gradle | 2 +- 8 files changed, 167 insertions(+), 98 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index 4c9319d0..616adbb9 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,24 +1,33 @@ apply plugin: 'com.android.application' +apply plugin: 'me.tatarka.retrolambda' android { compileSdkVersion 25 buildToolsVersion "25.0.3" + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + defaultConfig { applicationId "info.dvkr.screenstream" minSdkVersion 21 targetSdkVersion 25 - versionCode 22 - versionName "1.2.6" + versionCode 23 + versionName "1.2.7" resConfigs "en", "ru" } buildTypes { release { + buildConfigField "Boolean", "DEBUG_MODE", "false" minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' } debug { + buildConfigField "Boolean", "DEBUG_MODE", "true" minifyEnabled true shrinkResources true proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' @@ -37,9 +46,16 @@ android { dependencies { compile 'com.android.support:design:25.3.1' compile 'com.google.firebase:firebase-crash:10.2.4' - compile 'org.greenrobot:eventbus:3.0.0' + compile "io.reactivex.rxjava2:rxjava:2.1.0" + compile 'io.reactivex.rxjava2:rxandroid:2.0.1' + + compile 'org.greenrobot:eventbus:3.0.0' compile 'com.jrummyapps:colorpicker:2.1.7' + + debugCompile 'com.squareup.leakcanary:leakcanary-android:1.5.1' + releaseCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' + testCompile 'com.squareup.leakcanary:leakcanary-android-no-op:1.5.1' } apply plugin: 'com.google.gms.google-services' \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index aff3a914..83712e98 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -16,6 +16,10 @@ # public *; #} +# Retrolambda +-dontwarn java.lang.invoke.* +-dontwarn **$$Lambda$* + # EventBus 3.0 -keepattributes *Annotation* -keepclassmembers class * { diff --git a/app/src/main/java/info/dvkr/screenstream/ScreenStreamApplication.java b/app/src/main/java/info/dvkr/screenstream/ScreenStreamApplication.java index 1285928b..42a27ed8 100644 --- a/app/src/main/java/info/dvkr/screenstream/ScreenStreamApplication.java +++ b/app/src/main/java/info/dvkr/screenstream/ScreenStreamApplication.java @@ -1,11 +1,20 @@ package info.dvkr.screenstream; import android.app.Application; +import android.util.Log; + +import com.squareup.leakcanary.LeakCanary; +import com.squareup.leakcanary.RefWatcher; + +import java.io.IOException; +import java.net.SocketException; import info.dvkr.screenstream.data.AppData; import info.dvkr.screenstream.data.local.PreferencesHelper; import info.dvkr.screenstream.service.ForegroundService; import info.dvkr.screenstream.viewModel.MainActivityViewModel; +import io.reactivex.exceptions.UndeliverableException; +import io.reactivex.plugins.RxJavaPlugins; public class ScreenStreamApplication extends Application { @@ -15,11 +24,46 @@ public class ScreenStreamApplication extends Application { private MainActivityViewModel mMainActivityViewModel; private PreferencesHelper mPreferencesHelper; + private RefWatcher refWatcher; + @Override public void onCreate() { super.onCreate(); sAppInstance = this; + if (LeakCanary.isInAnalyzerProcess(this)) { + // This process is dedicated to LeakCanary for heap analysis. + // You should not init your app in this process. + return; + } + refWatcher = LeakCanary.install(this); + + RxJavaPlugins.setErrorHandler(e -> { + if (e instanceof UndeliverableException) { + e = e.getCause(); + } + if ((e instanceof IOException) || (e instanceof SocketException)) { + // fine, irrelevant network problem or API that throws on cancellation + return; + } + if (e instanceof InterruptedException) { + // fine, some blocking code was interrupted by a dispose call + return; + } + if ((e instanceof NullPointerException) || (e instanceof IllegalArgumentException)) { + // that's likely a bug in the application + Log.e(">>>>>> ", "likely a bug in the application", e); + return; + } + if (e instanceof IllegalStateException) { + // that's a bug in RxJava or in a custom operator + Log.e(">>>>>>", "a bug in RxJava or in a custom operator", e); + return; + } + Log.e(">>>>>>", "Undeliverable exception received, not sure what to do", e); + }); + + mAppData = new AppData(this); mMainActivityViewModel = new MainActivityViewModel(this); mPreferencesHelper = new PreferencesHelper(this); @@ -38,4 +82,8 @@ public static AppData getAppData() { public static PreferencesHelper getAppPreference() { return sAppInstance.mPreferencesHelper; } + + public static RefWatcher getRafWatcher() { + return sAppInstance.refWatcher; + } } \ No newline at end of file diff --git a/app/src/main/java/info/dvkr/screenstream/data/ImageGenerator.java b/app/src/main/java/info/dvkr/screenstream/data/ImageGenerator.java index 750146a7..aa01d67d 100644 --- a/app/src/main/java/info/dvkr/screenstream/data/ImageGenerator.java +++ b/app/src/main/java/info/dvkr/screenstream/data/ImageGenerator.java @@ -11,13 +11,14 @@ import android.os.Handler; import android.os.HandlerThread; import android.os.Process; +import android.util.Log; import com.google.firebase.crash.FirebaseCrash; import org.greenrobot.eventbus.EventBus; import java.io.ByteArrayOutputStream; -import java.io.IOException; +import java.util.concurrent.atomic.AtomicBoolean; import info.dvkr.screenstream.data.local.PreferencesHelper; import info.dvkr.screenstream.service.ForegroundService; @@ -26,137 +27,140 @@ import static info.dvkr.screenstream.ScreenStreamApplication.getAppPreference; public final class ImageGenerator { + private static final String TAG = ImageGenerator.class.getSimpleName(); private final Object mLock = new Object(); - - private volatile boolean isThreadRunning; + private final AtomicBoolean isPrepared = new AtomicBoolean(); private HandlerThread mImageThread; - private Handler mImageHandler; private ImageReader mImageReader; private VirtualDisplay mVirtualDisplay; private Bitmap mReusableBitmap; - private ByteArrayOutputStream mJpegOutputStream; private class ImageAvailableListener implements ImageReader.OnImageAvailableListener { - private Image mImage; - private Image.Plane mPlane; - private int mWidth; - private Bitmap mCleanBitmap; - private byte[] mJpegByteArray; + private final ByteArrayOutputStream mJpegOutputStream = new ByteArrayOutputStream(); + private final Matrix mMatrix; + + ImageAvailableListener(final Matrix matrix) { + this.mMatrix = matrix; + } @Override - public void onImageAvailable(ImageReader reader) { + public void onImageAvailable(final ImageReader reader) { synchronized (mLock) { - if (!isThreadRunning) return; +// Log.e(TAG, "ImageGenerator.onImageAvailable Tread: " + Thread.currentThread().getName()); + if (!isPrepared.get()) return; + + final Image image; try { - mImage = mImageReader.acquireLatestImage(); - } catch (UnsupportedOperationException e) { + image = reader.acquireLatestImage(); + } catch (UnsupportedOperationException e) { // TODO use onError EventBus.getDefault().postSticky(new BusMessages(BusMessages.MESSAGE_STATUS_IMAGE_GENERATOR_ERROR)); FirebaseCrash.report(e); return; } + if (image == null) return; - if (mImage == null) return; - - mPlane = mImage.getPlanes()[0]; - mWidth = mPlane.getRowStride() / mPlane.getPixelStride(); + final Image.Plane plane = image.getPlanes()[0]; + final int width = plane.getRowStride() / plane.getPixelStride(); - if (mWidth > mImage.getWidth()) { + final Bitmap cleanBitmap; + if (width > image.getWidth()) { if (mReusableBitmap == null) { - mReusableBitmap = Bitmap.createBitmap(mWidth, mImage.getHeight(), Bitmap.Config.ARGB_8888); + mReusableBitmap = Bitmap.createBitmap(width, image.getHeight(), Bitmap.Config.ARGB_8888); } - mReusableBitmap.copyPixelsFromBuffer(mPlane.getBuffer()); - mCleanBitmap = Bitmap.createBitmap(mReusableBitmap, 0, 0, mImage.getWidth(), mImage.getHeight()); + mReusableBitmap.copyPixelsFromBuffer(plane.getBuffer()); + cleanBitmap = Bitmap.createBitmap(mReusableBitmap, 0, 0, image.getWidth(), image.getHeight()); } else { - mCleanBitmap = Bitmap.createBitmap(mImage.getWidth(), mImage.getHeight(), Bitmap.Config.ARGB_8888); - mCleanBitmap.copyPixelsFromBuffer(mPlane.getBuffer()); + cleanBitmap = Bitmap.createBitmap(image.getWidth(), image.getHeight(), Bitmap.Config.ARGB_8888); + cleanBitmap.copyPixelsFromBuffer(plane.getBuffer()); } - Bitmap resizedBitmap; - if (getAppPreference().getResizeFactor() != PreferencesHelper.DEFAULT_RESIZE_FACTOR) { - float scale = getAppPreference().getResizeFactor() / 10f; - final Matrix matrix = new Matrix(); - matrix.postScale(scale, scale); - resizedBitmap = Bitmap.createBitmap(mCleanBitmap, 0, 0, mImage.getWidth(), mImage.getHeight(), matrix, false); - mCleanBitmap.recycle(); + final Bitmap resizedBitmap; + if (mMatrix.isIdentity()) { + resizedBitmap = cleanBitmap; } else { - resizedBitmap = mCleanBitmap; + resizedBitmap = Bitmap.createBitmap(cleanBitmap, 0, 0, image.getWidth(), image.getHeight(), mMatrix, false); + cleanBitmap.recycle(); } - mImage.close(); + image.close(); mJpegOutputStream.reset(); resizedBitmap.compress(Bitmap.CompressFormat.JPEG, getAppPreference().getJpegQuality(), mJpegOutputStream); resizedBitmap.recycle(); - mJpegByteArray = mJpegOutputStream.toByteArray(); + final byte[] jpegByteArray = mJpegOutputStream.toByteArray(); - if (mJpegByteArray != null) { + if (jpegByteArray != null) { // TODO use onNext if (getAppData().getImageQueue().size() > 3) { getAppData().getImageQueue().pollLast(); } - getAppData().getImageQueue().add(mJpegByteArray); - mJpegByteArray = null; + getAppData().getImageQueue().add(jpegByteArray); } } } } - public void start() { + public void start() throws IllegalStateException { synchronized (mLock) { - if (isThreadRunning) return; + Log.e(TAG, "ImageGenerator start: " + Thread.currentThread().getName()); + + if (isPrepared.get()) + throw new IllegalStateException("ImageGenerator is already running"); + final MediaProjection mediaProjection = ForegroundService.getMediaProjection(); - if (mediaProjection == null) return; + if (mediaProjection == null) throw new IllegalStateException("MediaProjection is null"); - mImageThread = new HandlerThread(ImageGenerator.class.getSimpleName(), - Process.THREAD_PRIORITY_MORE_FAVORABLE); + final Matrix matrix = new Matrix(); + if (getAppPreference().getResizeFactor() != PreferencesHelper.DEFAULT_RESIZE_FACTOR) { + final float scale = getAppPreference().getResizeFactor() / 10f; + matrix.postScale(scale, scale); + } + mImageThread = new HandlerThread(ImageGenerator.class.getSimpleName(), Process.THREAD_PRIORITY_MORE_FAVORABLE); mImageThread.start(); - mImageReader = ImageReader.newInstance(getAppData().getScreenSize().x, - getAppData().getScreenSize().y, - PixelFormat.RGBA_8888, 2); - mImageHandler = new Handler(mImageThread.getLooper()); - mJpegOutputStream = new ByteArrayOutputStream(); - mImageReader.setOnImageAvailableListener(new ImageAvailableListener(), mImageHandler); + mImageReader = ImageReader.newInstance(getAppData().getScreenSize().x, getAppData().getScreenSize().y, PixelFormat.RGBA_8888, 2); + mImageReader.setOnImageAvailableListener(new ImageAvailableListener(matrix), new Handler(mImageThread.getLooper())); mVirtualDisplay = mediaProjection.createVirtualDisplay("ScreenStreamVirtualDisplay", - getAppData().getScreenSize().x, - getAppData().getScreenSize().y, - getAppData().getScreenDensity(), + getAppData().getScreenSize().x, getAppData().getScreenSize().y, getAppData().getScreenDensity(), DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, mImageReader.getSurface(), - null, mImageHandler); + null, null); - isThreadRunning = true; + isPrepared.set(true); } } - public void stop() { + public void stop() throws IllegalStateException { synchronized (mLock) { - if (!isThreadRunning) return; + Log.e(TAG, "ImageGenerator stop: " + Thread.currentThread().getName()); + +// final RefWatcher refWatcher = getRafWatcher(); +// refWatcher.watch(mImageReader); - mImageReader.setOnImageAvailableListener(null, null); - mImageReader.close(); - mImageReader = null; + if (!isPrepared.get()) throw new IllegalStateException("ImageGenerator is not running"); + isPrepared.set(false); - try { - mJpegOutputStream.close(); - } catch (IOException e) { - FirebaseCrash.report(e); + if (mVirtualDisplay != null) { + mVirtualDisplay.release(); + mVirtualDisplay = null; } - mVirtualDisplay.release(); - mVirtualDisplay = null; + if (mImageReader != null) { + mImageReader.setOnImageAvailableListener(null, null); + mImageReader.close(); + mImageReader = null; + } - mImageHandler.removeCallbacksAndMessages(null); - mImageThread.quit(); - mImageThread = null; + if (mImageThread != null) { + mImageThread.quit(); + mImageThread = null; + } if (mReusableBitmap != null) { mReusableBitmap.recycle(); mReusableBitmap = null; } - - isThreadRunning = false; } } } \ No newline at end of file diff --git a/app/src/main/java/info/dvkr/screenstream/service/ForegroundService.java b/app/src/main/java/info/dvkr/screenstream/service/ForegroundService.java index f1045a62..96be6b33 100644 --- a/app/src/main/java/info/dvkr/screenstream/service/ForegroundService.java +++ b/app/src/main/java/info/dvkr/screenstream/service/ForegroundService.java @@ -23,7 +23,6 @@ import info.dvkr.screenstream.R; import info.dvkr.screenstream.data.BusMessages; import info.dvkr.screenstream.data.HttpServer; -import info.dvkr.screenstream.data.ImageGenerator; import info.dvkr.screenstream.data.NotifyImageGenerator; import info.dvkr.screenstream.view.MainActivity; @@ -55,7 +54,6 @@ public final class ForegroundService extends Service { private MediaProjection mMediaProjection; private MediaProjection.Callback mProjectionCallback; private HttpServer mHttpServer; - private ImageGenerator mImageGenerator; private NotifyImageGenerator mNotifyImageGenerator; private HandlerThread mHandlerThread; private ForegroundServiceHandler mForegroundServiceTaskHandler; @@ -85,10 +83,6 @@ public static MediaProjection getMediaProjection() { return sServiceInstance == null ? null : sServiceInstance.mMediaProjection; } - @Nullable - public static ImageGenerator getImageGenerator() { - return sServiceInstance == null ? null : sServiceInstance.mImageGenerator; - } @Override public void onCreate() { @@ -104,7 +98,7 @@ public void onStop() { } }; mHttpServer = new HttpServer(); - mImageGenerator = new ImageGenerator(); + getAppData().getImageQueue().clear(); mNotifyImageGenerator = new NotifyImageGenerator(getApplicationContext()); mNotifyImageGenerator.addDefaultScreen(); @@ -207,7 +201,10 @@ public void onDestroy() { stopForeground(true); unregisterReceiver(mBroadcastReceiver); unregisterReceiver(mLocalNotificationReceiver); - if (mMediaProjection != null) mMediaProjection.unregisterCallback(mProjectionCallback); + if (mMediaProjection != null) { + mMediaProjection.unregisterCallback(mProjectionCallback); + mMediaProjection.stop(); + } mHandlerThread.quit(); } @@ -250,7 +247,10 @@ private void serviceStopStreaming() { stopForeground(true); mForegroundServiceTaskHandler.obtainMessage(ForegroundServiceHandler.HANDLER_STOP_STREAMING).sendToTarget(); startForeground(NOTIFICATION_START_STREAMING, getNotificationStart()); - if (mMediaProjection != null) mMediaProjection.unregisterCallback(mProjectionCallback); + if (mMediaProjection != null) { + mMediaProjection.unregisterCallback(mProjectionCallback); + mMediaProjection.stop(); + } getAppData().getImageQueue().clear(); mNotifyImageGenerator.addDefaultScreen(); diff --git a/app/src/main/java/info/dvkr/screenstream/service/ForegroundServiceHandler.java b/app/src/main/java/info/dvkr/screenstream/service/ForegroundServiceHandler.java index af6ead8c..bb3d9984 100644 --- a/app/src/main/java/info/dvkr/screenstream/service/ForegroundServiceHandler.java +++ b/app/src/main/java/info/dvkr/screenstream/service/ForegroundServiceHandler.java @@ -12,7 +12,6 @@ import static android.view.Surface.ROTATION_0; import static android.view.Surface.ROTATION_180; import static info.dvkr.screenstream.ScreenStreamApplication.getAppData; -import static info.dvkr.screenstream.service.ForegroundService.getImageGenerator; import static info.dvkr.screenstream.service.ForegroundService.getMediaProjection; final class ForegroundServiceHandler extends Handler { @@ -24,56 +23,52 @@ final class ForegroundServiceHandler extends Handler { private static final int HANDLER_DETECT_ROTATION = 20; private boolean mCurrentOrientation; + private final ImageGenerator mImageGenerator; ForegroundServiceHandler(final Looper looper) { super(looper); + mImageGenerator = new ImageGenerator(); } @Override public void handleMessage(Message message) { - ImageGenerator imageGenerator; - switch (message.what) { case HANDLER_START_STREAMING: if (getAppData().isStreamRunning()) break; removeMessages(HANDLER_DETECT_ROTATION); mCurrentOrientation = getOrientation(); - imageGenerator = getImageGenerator(); - if (imageGenerator != null) imageGenerator.start(); + mImageGenerator.start(); sendMessageDelayed(obtainMessage(HANDLER_DETECT_ROTATION), 250); getAppData().setStreamRunning(true); break; case HANDLER_PAUSE_STREAMING: if (!getAppData().isStreamRunning()) break; - imageGenerator = getImageGenerator(); - if (imageGenerator != null) imageGenerator.stop(); + mImageGenerator.stop(); sendMessageDelayed(obtainMessage(HANDLER_RESUME_STREAMING), 250); break; case HANDLER_RESUME_STREAMING: if (!getAppData().isStreamRunning()) break; - imageGenerator = getImageGenerator(); - if (imageGenerator != null) imageGenerator.start(); + mImageGenerator.start(); sendMessageDelayed(obtainMessage(HANDLER_DETECT_ROTATION), 250); break; case HANDLER_STOP_STREAMING: if (!getAppData().isStreamRunning()) break; removeMessages(HANDLER_DETECT_ROTATION); removeMessages(HANDLER_STOP_STREAMING); - imageGenerator = getImageGenerator(); - if (imageGenerator != null) imageGenerator.stop(); + mImageGenerator.stop(); final MediaProjection mediaProjection = getMediaProjection(); if (mediaProjection != null) mediaProjection.stop(); getAppData().setStreamRunning(false); break; case HANDLER_DETECT_ROTATION: if (!getAppData().isStreamRunning()) break; - boolean newOrientation = getOrientation(); - if (mCurrentOrientation == newOrientation) { + final boolean newOrientation = getOrientation(); + if (mCurrentOrientation != newOrientation) { + mCurrentOrientation = newOrientation; + obtainMessage(HANDLER_PAUSE_STREAMING).sendToTarget(); + } else { sendMessageDelayed(obtainMessage(HANDLER_DETECT_ROTATION), 250); - break; } - mCurrentOrientation = newOrientation; - obtainMessage(HANDLER_PAUSE_STREAMING).sendToTarget(); break; default: FirebaseCrash.log("Cannot handle message"); diff --git a/app/src/main/java/info/dvkr/screenstream/view/MainActivity.java b/app/src/main/java/info/dvkr/screenstream/view/MainActivity.java index dcc6c0a6..f3eb327e 100644 --- a/app/src/main/java/info/dvkr/screenstream/view/MainActivity.java +++ b/app/src/main/java/info/dvkr/screenstream/view/MainActivity.java @@ -184,4 +184,6 @@ public void onActivityResult(int requestCode, int resultCode, Intent data) { FirebaseCrash.log("Unknown request code: " + requestCode); } } + + } \ No newline at end of file diff --git a/build.gradle b/build.gradle index a0acab93..70ac7519 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { dependencies { classpath 'com.android.tools.build:gradle:2.3.1' classpath 'com.google.gms:google-services:3.0.0' - + classpath 'me.tatarka:gradle-retrolambda:3.6.1' // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files }