diff options
28 files changed, 723 insertions, 241 deletions
@@ -1,11 +1,14 @@ *.iml .gradle /local.properties -/.idea/caches/build_file_checksums.ser +/.idea/caches /.idea/libraries /.idea/modules.xml /.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml .DS_Store /build /captures .externalNativeBuild +.cxx diff --git a/.idea/codeStyles/Project.xml b/.idea/codeStyles/Project.xml new file mode 100644 index 0000000..681f41a --- /dev/null +++ b/.idea/codeStyles/Project.xml @@ -0,0 +1,116 @@ +<component name="ProjectCodeStyleConfiguration"> + <code_scheme name="Project" version="173"> + <codeStyleSettings language="XML"> + <indentOptions> + <option name="CONTINUATION_INDENT_SIZE" value="4" /> + </indentOptions> + <arrangement> + <rules> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:android</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>xmlns:.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:id</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*:name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>name</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>style</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>^$</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>http://schemas.android.com/apk/res/android</XML_NAMESPACE> + </AND> + </match> + <order>ANDROID_ATTRIBUTE_ORDER</order> + </rule> + </section> + <section> + <rule> + <match> + <AND> + <NAME>.*</NAME> + <XML_ATTRIBUTE /> + <XML_NAMESPACE>.*</XML_NAMESPACE> + </AND> + </match> + <order>BY_NAME</order> + </rule> + </section> + </rules> + </arrangement> + </codeStyleSettings> + </code_scheme> +</component>
\ No newline at end of file diff --git a/.idea/gradle.xml b/.idea/gradle.xml new file mode 100644 index 0000000..d291b3d --- /dev/null +++ b/.idea/gradle.xml @@ -0,0 +1,16 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="GradleSettings"> + <option name="linkedExternalProjectsSettings"> + <GradleProjectSettings> + <compositeConfiguration> + <compositeBuild compositeDefinitionSource="SCRIPT" /> + </compositeConfiguration> + <option name="distributionType" value="DEFAULT_WRAPPED" /> + <option name="externalProjectPath" value="$PROJECT_DIR$" /> + <option name="resolveModulePerSourceSet" value="false" /> + <option name="testRunner" value="PLATFORM" /> + </GradleProjectSettings> + </option> + </component> +</project>
\ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..37a7509 --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,9 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="ProjectRootManager" version="2" languageLevel="JDK_1_7" project-jdk-name="1.8" project-jdk-type="JavaSDK"> + <output url="file://$PROJECT_DIR$/build/classes" /> + </component> + <component name="ProjectType"> + <option name="id" value="Android" /> + </component> +</project>
\ No newline at end of file diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml new file mode 100644 index 0000000..7f68460 --- /dev/null +++ b/.idea/runConfigurations.xml @@ -0,0 +1,12 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="RunConfigurationProducerService"> + <option name="ignoredProducers"> + <set> + <option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" /> + <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" /> + <option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" /> + </set> + </option> + </component> +</project>
\ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..35eb1dd --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="UTF-8"?> +<project version="4"> + <component name="VcsDirectoryMappings"> + <mapping directory="" vcs="Git" /> + </component> +</project>
\ No newline at end of file diff --git a/app/build.gradle b/app/build.gradle index 523c8a4..e13abab 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,14 +1,14 @@ apply plugin: 'com.android.application' android { - compileSdkVersion 28 + compileSdkVersion 29 defaultConfig { applicationId "com.javispedro.vndroid" minSdkVersion 26 - targetSdkVersion 28 + targetSdkVersion 29 versionCode 1 versionName "1.0" - testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" ndk { //abiFilters 'x86' } @@ -34,11 +34,9 @@ android { } dependencies { - implementation fileTree(include: ['*.jar'], dir: 'libs') - implementation 'com.android.support:appcompat-v7:28.0.0' - implementation 'com.android.support.constraint:constraint-layout:1.1.3' - implementation 'com.android.support:support-v4:28.0.0' - testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support.test:runner:1.0.2' - androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.2' + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation 'androidx.appcompat:appcompat:1.0.2' + implementation 'androidx.constraintlayout:constraintlayout:1.1.3' + implementation 'com.google.android.material:material:1.0.0' + implementation 'androidx.preference:preference:1.1.0-alpha05' } diff --git a/app/libs/.gitignore b/app/libs/.gitignore new file mode 100644 index 0000000..b48c09f --- /dev/null +++ b/app/libs/.gitignore @@ -0,0 +1,3 @@ +libjpeg-turbo* +libvncserver* +openssl* diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5786f0f..2ad6c82 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -12,7 +12,10 @@ android:roundIcon="@mipmap/ic_launcher_round" android:supportsRtl="true" android:theme="@style/AppTheme"> - <activity android:name=".SetupActivity"> + + <activity + android:name=".SettingsActivity" + android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN" /> <category android:name="android.intent.category.LAUNCHER" /> @@ -22,8 +25,8 @@ <service android:name=".ServerService" android:enabled="true" - android:exported="true" /> - + android:exported="true" + android:foregroundServiceType="mediaProjection" /> <service android:name=".ControlService" android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE"> @@ -35,7 +38,6 @@ android:name="android.accessibilityservice" android:resource="@xml/controlservice" /> </service> - </application> </manifest>
\ No newline at end of file diff --git a/app/src/main/cpp/native-lib.cpp b/app/src/main/cpp/native-lib.cpp index 3a1811c..6216181 100644 --- a/app/src/main/cpp/native-lib.cpp +++ b/app/src/main/cpp/native-lib.cpp @@ -41,6 +41,7 @@ struct Data jobject cb_obj; jmethodID cb_ev_ptr; jmethodID cb_ev_key; + jmethodID cb_ev_client; std::atomic<bool> pendingPtrEvent; int ptrButtonMask, ptrX, ptrY; @@ -48,6 +49,10 @@ struct Data std::atomic<bool> pendingKeyEvent; rfbKeySym key; bool keyState; + + std::atomic<bool> pendingClientEvent; + + unsigned int numClients; }; static inline Data * getData(JNIEnv *env, jobject instance) @@ -71,6 +76,16 @@ static inline Data * getData(rfbClientPtr client) return data; } +static void listen_thread_client_gone(rfbClientPtr client) +{ + Data *data = getData(client); + + data->numClients--; + data->pendingClientEvent = true; + + eventfd_write(data->eventEventFd, 1); +} + static void listen_thread_main(Data *data, JavaVM *vm) { rfbScreenInfoPtr screen = data->screen; @@ -104,11 +119,20 @@ static void listen_thread_main(Data *data, JavaVM *vm) else if (data->listenEventFd >= 0 && FD_ISSET(data->listenEventFd, &listen_fds)) break; - rfbClientPtr cl = NULL; - if(client_fd >= 0) - cl = rfbNewClient(screen,client_fd); - if (cl && !cl->onHold ) - rfbStartOnHoldClient(cl); + if(client_fd >= 0) { + rfbClientPtr cl = rfbNewClient(screen, client_fd); + if (cl) { + cl->clientGoneHook = listen_thread_client_gone; + + data->numClients++; + data->pendingClientEvent = true; + + eventfd_write(data->eventEventFd, 1); + + if (!cl->onHold) + rfbStartOnHoldClient(cl); + } + } } } @@ -134,6 +158,11 @@ static void event_thread_main(Data *data, JavaVM *vm) env->CallVoidMethod(data->cb_obj, data->cb_ev_key, (jint)data->key, (jboolean)data->keyState); data->pendingKeyEvent = false; } + + if (data->pendingClientEvent) { + env->CallVoidMethod(data->cb_obj, data->cb_ev_client); + data->pendingClientEvent = false; + } } vm->DetachCurrentThread(); @@ -150,6 +179,7 @@ static void ptr_event(int buttonMask, int x, int y, rfbClientPtr cl) eventfd_write(data->eventEventFd, 1); + // Still send the event to RFB, to do server-side cursor processing rfbDefaultPtrAddEvent(buttonMask, x, y, cl); } @@ -199,10 +229,13 @@ Java_com_javispedro_vndroid_RFBServer_allocate(JNIEnv *env, jobject instance) assert(data->cb_ev_ptr); data->cb_ev_key = env->GetMethodID(cb_cls, "onKeyEvent", "(IZ)V"); assert(data->cb_ev_key); + data->cb_ev_client = env->GetMethodID(cb_cls, "onClientEvent", "()V"); + assert(data->cb_ev_client); data->screen = 0; data->listenEventFd = -1; data->eventEventFd = -1; + data->numClients = 0; env->SetLongField(instance, fid, reinterpret_cast<jlong>(data)); @@ -359,3 +392,10 @@ Java_com_javispedro_vndroid_RFBServer_put_1image(JNIEnv *env, jobject instance, return JNI_TRUE; } + +extern "C" +JNIEXPORT jint JNICALL +Java_com_javispedro_vndroid_RFBServer_get_1num_1clients(JNIEnv *env, jobject instance) { + Data *data = getData(env, instance); + return data->numClients; +}
\ No newline at end of file diff --git a/app/src/main/java/com/javispedro/vndroid/ControlService.java b/app/src/main/java/com/javispedro/vndroid/ControlService.java index db5362d..166e379 100644 --- a/app/src/main/java/com/javispedro/vndroid/ControlService.java +++ b/app/src/main/java/com/javispedro/vndroid/ControlService.java @@ -2,10 +2,11 @@ package com.javispedro.vndroid; import android.accessibilityservice.AccessibilityService; import android.content.Intent; -import android.support.annotation.Nullable; import android.util.Log; import android.view.accessibility.AccessibilityEvent; +import androidx.annotation.Nullable; + public class ControlService extends AccessibilityService { private final String TAG = ControlService.class.getSimpleName(); diff --git a/app/src/main/java/com/javispedro/vndroid/KeyEventOutput.java b/app/src/main/java/com/javispedro/vndroid/KeyEventOutput.java index 597942a..3ec4319 100644 --- a/app/src/main/java/com/javispedro/vndroid/KeyEventOutput.java +++ b/app/src/main/java/com/javispedro/vndroid/KeyEventOutput.java @@ -1,10 +1,11 @@ package com.javispedro.vndroid; import android.os.Bundle; -import android.support.annotation.Nullable; import android.util.Log; import android.view.accessibility.AccessibilityNodeInfo; +import androidx.annotation.Nullable; + import com.javispedro.vndroid.keymaps.KeyActionHandler; import com.javispedro.vndroid.keymaps.KeyHandler; diff --git a/app/src/main/java/com/javispedro/vndroid/RFBServer.java b/app/src/main/java/com/javispedro/vndroid/RFBServer.java index f5af05c..8838818 100644 --- a/app/src/main/java/com/javispedro/vndroid/RFBServer.java +++ b/app/src/main/java/com/javispedro/vndroid/RFBServer.java @@ -2,8 +2,8 @@ package com.javispedro.vndroid; import android.graphics.PixelFormat; import android.media.Image; -import android.support.annotation.Nullable; -import android.util.EventLog; + +import androidx.annotation.Nullable; import java.nio.ByteBuffer; @@ -25,6 +25,7 @@ public class RFBServer { public interface EventCallback { void onPointerEvent(int buttonMask, int x, int y); void onKeyEvent(int key, boolean state); + void onClientEvent(); } public RFBServer() { @@ -40,13 +41,13 @@ public class RFBServer { } public void stop() { - forgetLastImage(); shutdown(); + forgetLastImage(); } public void finalize() { - forgetLastImage(); shutdown(); + forgetLastImage(); deallocate(); } @@ -77,6 +78,10 @@ public class RFBServer { lastImage = image; } + public int getNumClients() { + return get_num_clients(); + } + private void forgetLastImage() { if (lastImage != null) { lastImage.close(); @@ -95,4 +100,6 @@ public class RFBServer { private native void set_event_callback(EventCallback c); private native boolean put_image(int width, int height, ByteBuffer buffer, int pixel_stride, int row_stride); + + private native int get_num_clients(); } diff --git a/app/src/main/java/com/javispedro/vndroid/ScreenGrabber.java b/app/src/main/java/com/javispedro/vndroid/ScreenGrabber.java index 54d11bc..a5701b3 100644 --- a/app/src/main/java/com/javispedro/vndroid/ScreenGrabber.java +++ b/app/src/main/java/com/javispedro/vndroid/ScreenGrabber.java @@ -5,9 +5,10 @@ import android.content.ContextWrapper; import android.hardware.display.VirtualDisplay; import android.media.Image; import android.media.ImageReader; -import android.support.annotation.Nullable; import android.util.Log; +import androidx.annotation.Nullable; + public abstract class ScreenGrabber extends ContextWrapper { private static final String TAG = ScreenGrabber.class.getSimpleName(); diff --git a/app/src/main/java/com/javispedro/vndroid/ScreenMirrorGrabber.java b/app/src/main/java/com/javispedro/vndroid/ScreenMirrorGrabber.java index 9f78b3c..c223c65 100644 --- a/app/src/main/java/com/javispedro/vndroid/ScreenMirrorGrabber.java +++ b/app/src/main/java/com/javispedro/vndroid/ScreenMirrorGrabber.java @@ -1,34 +1,50 @@ package com.javispedro.vndroid; +import android.app.Activity; import android.content.Context; +import android.content.Intent; import android.graphics.PixelFormat; import android.hardware.display.DisplayManager; import android.media.ImageReader; import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; import android.os.Handler; -import android.support.annotation.NonNull; import android.util.DisplayMetrics; import android.util.Log; import android.view.Display; +import android.view.WindowManager; public class ScreenMirrorGrabber extends ScreenGrabber { private static final String TAG = ScreenMirrorGrabber.class.getSimpleName(); - protected final MediaProjection projection; - protected final Display realDisplay; + protected MediaProjection projection; - protected final DisplayMetrics realDisplayMetrics; + protected int projAskCode; + protected Intent projAskData; + + protected Display realDisplay; + protected DisplayMetrics realDisplayMetrics; private float scale = 0.5f; - public ScreenMirrorGrabber(Context context, @NonNull MediaProjection proj, @NonNull Display display) { + public ScreenMirrorGrabber(Context context, MediaProjection proj) { super(context); projection = proj; - realDisplay = display; - realDisplayMetrics = new DisplayMetrics(); + projAskCode = 0; + projAskData = null; // Already obtained + } + + public ScreenMirrorGrabber(Context context, int projectionResultCode, Intent projectionResultData) { + super(context); + projection = null; + projAskCode = projectionResultCode; + projAskData = projectionResultData; } private void initDisplay() { + WindowManager wm = getSystemService(WindowManager.class); + realDisplay = wm.getDefaultDisplay(); + realDisplayMetrics = new DisplayMetrics(); realDisplay.getRealMetrics(realDisplayMetrics); Log.d(TAG, "real display size: " + realDisplayMetrics.widthPixels + "x" + realDisplayMetrics.heightPixels); @@ -55,6 +71,12 @@ public class ScreenMirrorGrabber extends ScreenGrabber { Log.w(TAG, "already started"); return; } + if (projection == null && projAskCode == Activity.RESULT_OK) { + MediaProjectionManager manager = getSystemService(MediaProjectionManager.class); + projection = manager.getMediaProjection(projAskCode, projAskData); + projAskCode = 0; + projAskData = null; + } initDisplay(); } diff --git a/app/src/main/java/com/javispedro/vndroid/ServerRunningNotification.java b/app/src/main/java/com/javispedro/vndroid/ServerRunningNotification.java index 6fcf8ab..224df0e 100644 --- a/app/src/main/java/com/javispedro/vndroid/ServerRunningNotification.java +++ b/app/src/main/java/com/javispedro/vndroid/ServerRunningNotification.java @@ -1,6 +1,5 @@ package com.javispedro.vndroid; -import android.annotation.TargetApi; import android.app.Notification; import android.app.NotificationChannel; import android.app.NotificationManager; @@ -8,11 +7,8 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.res.Resources; -import android.graphics.Bitmap; -import android.graphics.BitmapFactory; -import android.net.Uri; -import android.os.Build; -import android.support.v4.app.NotificationCompat; + +import androidx.core.app.NotificationCompat; /** * Helper class for showing and canceling server running @@ -86,7 +82,7 @@ public class ServerRunningNotification { PendingIntent.getActivity( context, 0, - new Intent(context, SetupActivity.class), + new Intent(context, SettingsActivity.class), PendingIntent.FLAG_UPDATE_CURRENT)) // Example additional actions for this notification. These will @@ -100,7 +96,7 @@ public class ServerRunningNotification { PendingIntent.getForegroundService( context, 0, - new Intent(context, ServerService.class).setAction(ServerService.ACTION_STOP), + new Intent(context, ServerService.class).setAction(ServerService.ACTION_STOP_SERVER), PendingIntent.FLAG_UPDATE_CURRENT)); return builder.build(); diff --git a/app/src/main/java/com/javispedro/vndroid/ServerService.java b/app/src/main/java/com/javispedro/vndroid/ServerService.java index bf72d21..96420df 100644 --- a/app/src/main/java/com/javispedro/vndroid/ServerService.java +++ b/app/src/main/java/com/javispedro/vndroid/ServerService.java @@ -1,61 +1,80 @@ package com.javispedro.vndroid; -import android.app.Activity; import android.app.Notification; import android.app.Service; import android.content.Intent; import android.content.res.Configuration; import android.media.Image; -import android.media.projection.MediaProjection; -import android.media.projection.MediaProjectionManager; +import android.os.Binder; import android.os.IBinder; import android.util.Log; -import android.view.WindowManager; import android.widget.Toast; import com.javispedro.vndroid.keymaps.AndroidKeyHandler; import com.javispedro.vndroid.keymaps.SpanishKeyHandler; +import java.lang.ref.WeakReference; +import java.net.InetAddress; +import java.net.NetworkInterface; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; + public class ServerService extends Service { private static final String TAG = ServerService.class.getSimpleName(); - public static final String ACTION_START = "ACTION_START"; - public static final String ACTION_STOP = "ACTION_STOP"; - public static final String ACTION_NOTIFY_MEDIA_PROJECTION_RESULT = "ACTION_NOTIFY_MEDIA_PROJECTION_RESULT"; + + public static final String ACTION_START_SERVER = "ACTION_START_SERVER"; + public static final String ACTION_STOP_SERVER = "ACTION_STOP_SERVER"; + + public class ServerBinder extends Binder { + ServerService getService() { + return ServerService.this; + } + } + public interface ServerStatusCallback { + void onServerStatusChanged(); + void onNumClientChanged(); + } + + private ServerBinder binder; + private WeakReference<ServerStatusCallback> callback; private ScreenGrabber screenGrabber; private RFBServer rfbServer; private PointerEventOutput pointerOut; private KeyEventOutput keyOut; - public ServerService() { - } - @Override public void onCreate() { + Log.d(TAG, "onCreate"); + binder = new ServerBinder(); ServerRunningNotification.initNotificationChannel(this); System.loadLibrary("native-lib"); } @Override + public void onDestroy() { + Log.d(TAG, "onDestroy"); + cleanupServer(); + binder = null; + callback = null; + } + + @Override public IBinder onBind(Intent intent) { - throw new UnsupportedOperationException("Not implemented"); + return binder; } @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null) { - switch (intent.getAction()) { - case ACTION_START: - start(); - break; - case ACTION_STOP: - stop(); - break; - case ACTION_NOTIFY_MEDIA_PROJECTION_RESULT: - notifyMediaProjectionResult(intent.getIntExtra("resultCode", Activity.RESULT_CANCELED), - (Intent) intent.getParcelableExtra("resultData")); - break; - } + Log.d(TAG, "onStartCommand intent=" + intent); + switch (intent.getAction()) { + case ACTION_START_SERVER: + startServer(); + return START_REDELIVER_INTENT; + case ACTION_STOP_SERVER: + stopServer(); + return START_NOT_STICKY; } return super.onStartCommand(intent, flags, startId); } @@ -69,43 +88,15 @@ public class ServerService extends Service { } } - protected class ScreenGrabberCallback implements ScreenGrabber.Callback { - @Override - public void onImage(Image image) { - if (rfbServer != null) { - rfbServer.putImage(image); - } else { - image.close(); - } + public void setServerStatusCallback(ServerStatusCallback callback) { + if (callback != null) { + this.callback = new WeakReference<>(callback); + } else { + this.callback = null; } } - protected class EventCallback implements RFBServer.EventCallback { - @Override - public void onPointerEvent(int buttonMask, int x, int y) { - try { - x = screenGrabber.scaleInputX(x); - y = screenGrabber.scaleInputY(y); - pointerOut.postPointerEvent((byte) buttonMask, x, y); - } catch (Exception e) { - Log.e(TAG, "Exception on pointer EventCallback: " + e.toString()); - e.printStackTrace(); - // Need to supress the exception, otherwise we'll crash JNI - } - } - - @Override - public void onKeyEvent(int key, boolean state) { - try { - keyOut.postKeyEvent(key, state); - } catch (Exception e) { - Log.e(TAG, "Exception on key EventCallback: " + e.toString()); - e.printStackTrace(); - } - } - } - - protected void start() { + public void startServer() { if (rfbServer != null) { Log.w(TAG, "cannot start, already started"); return; @@ -113,6 +104,9 @@ public class ServerService extends Service { Log.d(TAG, "starting"); + Notification notification = ServerRunningNotification.build(this); + startForeground(1, notification); + if (!ControlService.isServiceStarted()) { Toast toast = Toast.makeText(this, R.string.toast_no_input_service, Toast.LENGTH_SHORT); toast.show(); @@ -123,7 +117,6 @@ public class ServerService extends Service { keyOut.addHandler(new AndroidKeyHandler()); keyOut.addHandler(new SpanishKeyHandler()); - if (screenGrabber == null) { screenGrabber = new ScreenVirtualGrabber(this); } @@ -135,13 +128,61 @@ public class ServerService extends Service { screenGrabber.setCallback(new ScreenGrabberCallback()); screenGrabber.start(); - Notification notification = ServerRunningNotification.build(this); - startForeground(1, notification); + notifyServerStatusChanged(); } - protected void stop() { + public void stopServer() { Log.d(TAG, "stopping"); + cleanupServer(); + + stopForeground(true); + stopSelf(); + + notifyServerStatusChanged(); + } + + public boolean isServerActive() { + return rfbServer != null; + } + + public void setMediaProjectionResult(int resultCode, Intent data) { + if (screenGrabber != null) { + Log.w(TAG, "already have an screen grabber"); + } + + screenGrabber = new ScreenMirrorGrabber(this, resultCode, data); + } + + public int getListeningDisplay() { + return 0; + } + + public List<String> getListeningIPAddresses() { + LinkedList<String> result = new LinkedList<String>(); + try { + for (Enumeration<NetworkInterface> netif_it = NetworkInterface.getNetworkInterfaces(); netif_it.hasMoreElements(); ) { + NetworkInterface netif = netif_it.nextElement(); + + for (Enumeration<InetAddress> addr_it = netif.getInetAddresses(); addr_it.hasMoreElements(); ) { + InetAddress addr = addr_it.nextElement(); + + if (addr.isLoopbackAddress()) continue; + + result.add(addr.getHostAddress()); + } + } + } catch (Exception ex) { + Log.w(TAG, "While enumerating IP addresses: " + ex.toString()); + } + return result; + } + + public int getNumClients() { + return rfbServer.getNumClients(); + } + + private void cleanupServer() { if (rfbServer != null) { rfbServer.stop(); rfbServer = null; @@ -156,20 +197,67 @@ public class ServerService extends Service { if (keyOut != null) { keyOut = null; } + } - stopForeground(true); - stopSelf(); + private ServerStatusCallback getStatusCallback() { + return callback == null ? null : callback.get(); } - protected void notifyMediaProjectionResult(int resultCode, Intent data) { - if (screenGrabber != null) { - Log.w(TAG, "already have an screen grabber"); + private void notifyServerStatusChanged() { + ServerStatusCallback cb = getStatusCallback(); + if (cb == null) return; + cb.onServerStatusChanged(); + } + + private void notifyNumClientChanged() { + ServerStatusCallback cb = getStatusCallback(); + if (cb == null) return; + cb.onNumClientChanged(); + } + + private class ScreenGrabberCallback implements ScreenGrabber.Callback { + @Override + public void onImage(Image image) { + if (rfbServer != null) { + rfbServer.putImage(image); + } else { + image.close(); + } } - MediaProjectionManager manager = getSystemService(MediaProjectionManager.class); - MediaProjection projection = manager.getMediaProjection(resultCode, data); + } - WindowManager wm = getSystemService(WindowManager.class); + private class EventCallback implements RFBServer.EventCallback { + @Override + public void onPointerEvent(int buttonMask, int x, int y) { + try { + x = screenGrabber.scaleInputX(x); + y = screenGrabber.scaleInputY(y); + pointerOut.postPointerEvent((byte) buttonMask, x, y); + } catch (Exception e) { + Log.e(TAG, "Exception on pointer EventCallback: " + e.toString()); + e.printStackTrace(); + // Need to suppress the exception, otherwise we'll crash JNI + } + } - screenGrabber = new ScreenMirrorGrabber(this, projection, wm.getDefaultDisplay()); + @Override + public void onKeyEvent(int key, boolean state) { + try { + keyOut.postKeyEvent(key, state); + } catch (Exception e) { + Log.e(TAG, "Exception on key EventCallback: " + e.toString()); + e.printStackTrace(); + } + } + + @Override + public void onClientEvent() { + try { + notifyNumClientChanged(); + } catch (Exception e) { + Log.e(TAG, "Exception on client EventCallback: " + e.toString()); + e.printStackTrace(); + } + } } } diff --git a/app/src/main/java/com/javispedro/vndroid/SettingsActivity.java b/app/src/main/java/com/javispedro/vndroid/SettingsActivity.java new file mode 100644 index 0000000..676af3d --- /dev/null +++ b/app/src/main/java/com/javispedro/vndroid/SettingsActivity.java @@ -0,0 +1,213 @@ +package com.javispedro.vndroid; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.projection.MediaProjectionManager; +import android.os.Bundle; +import android.os.IBinder; +import android.util.Log; +import android.widget.Toast; + +import androidx.annotation.Nullable; +import androidx.appcompat.app.AppCompatActivity; +import androidx.preference.Preference; +import androidx.preference.PreferenceFragmentCompat; +import androidx.preference.TwoStatePreference; + +import java.util.List; + +public class SettingsActivity extends AppCompatActivity { + private static final String TAG = SettingsActivity.class.getSimpleName(); + + private static final int REQUEST_MEDIA_PROJECTION = 1; + + private static final boolean mirrorScreenMode = true; + + + public static class SettingsFragment extends PreferenceFragmentCompat implements ServerService.ServerStatusCallback { + private ServerConnection serverConnection = null; + + @Override + public void onCreate(@Nullable Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + } + + @Override + public void onCreatePreferences(Bundle savedInstanceState, String rootKey) { + setPreferencesFromResource(R.xml.root_preferences, rootKey); + findPreference(getString(R.string.settings_enable_key)).setOnPreferenceClickListener(new Preference.OnPreferenceClickListener() { + @Override + public boolean onPreferenceClick(Preference preference) { + TwoStatePreference pref = (TwoStatePreference) preference; + setServerEnabled(pref.isChecked()); + return true; + } + }); + } + + @Override + public void onStart() { + super.onStart(); + serverConnection = new ServerConnection(); + serverConnection.bind(requireActivity(), this); + } + + @Override + public void onStop() { + serverConnection.close(requireActivity()); + serverConnection = null; + super.onStop(); + } + + @Override + public void onActivityResult(int requestCode, int resultCode, Intent data) { + switch (requestCode) { + case REQUEST_MEDIA_PROJECTION: + notifyMediaProjectionResult(resultCode, data); + return; + } + super.onActivityResult(requestCode, resultCode, data); + } + + @Override + public void onServerStatusChanged() { + postUpdateServerStatus(); + } + + @Override + public void onNumClientChanged() { + postUpdateServerStatus(); + } + + private void setServerEnabled(boolean state) { + Log.d(TAG, "setServerEnabled: " + state); + ServerService server = serverConnection.getServer(); + if (state) { + if (server == null) { + throw new IllegalStateException("ServerService not bound"); + } + if (server.isServerActive()) return; + + if (mirrorScreenMode) { + MediaProjectionManager manager = requireContext().getSystemService(MediaProjectionManager.class); + startActivityForResult(manager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); + } else { + server.startServer(); + } + } else { + if (server == null) return; + server.stopServer(); + } + } + + private void notifyMediaProjectionResult(int resultCode, Intent resultData) { + if (resultCode != Activity.RESULT_OK) { + Log.w(TAG, "User cancelled media projection"); + Toast.makeText(requireContext(), getString(R.string.toast_no_mirror_permission), Toast.LENGTH_SHORT).show(); + updateServerStatus(); + return; + } + + ServerService server = serverConnection.getServer(); + if (server == null) { + Log.e(TAG, "server died before projection could be sent"); + updateServerStatus(); + return; + } + if (server.isServerActive()) { + Log.w(TAG, "server already active"); + updateServerStatus(); + return; + } + + server.setMediaProjectionResult(resultCode, resultData); + server.startServer(); + } + + private void updateServerStatus() { + TwoStatePreference enablePref = findPreference(getString(R.string.settings_enable_key)); + Preference statusPref = findPreference(getString(R.string.settings_status_key)); + ServerService server = serverConnection.getServer(); + if (server != null && server.isServerActive()) { + enablePref.setChecked(true); + StringBuilder sb = new StringBuilder(); + int display = server.getListeningDisplay(); + List<String> ips = server.getListeningIPAddresses(); + if (!ips.isEmpty()) { + sb.append('\n'); + sb.append(getString(R.string.status_server_addresses)); + for (String ip : ips) { + sb.append(' '); + sb.append(ip); + sb.append(':'); + sb.append(display); + } + } + int numClients = server.getNumClients(); + if (numClients > 0) { + sb.append('\n'); + sb.append(getResources().getQuantityString(R.plurals.status_num_clients, numClients, numClients)); + } + statusPref.setSummary(sb.toString()); + } else { + enablePref.setChecked(false); + statusPref.setSummary(""); + } + } + + private void postUpdateServerStatus() { + requireActivity().runOnUiThread(new Runnable() { + public void run() { + updateServerStatus(); + } + }); + } + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.settings_activity); + getSupportFragmentManager() + .beginTransaction() + .replace(R.id.settings, new SettingsFragment()) + .commit(); + } + + private static class ServerConnection implements ServiceConnection { + private ServerService server = null; + private ServerService.ServerStatusCallback callback = null; + + public void bind(Activity activity, ServerService.ServerStatusCallback callback) { + this.callback = callback; + activity.bindService(new Intent(activity, ServerService.class), this, Context.BIND_AUTO_CREATE); + } + + public void close(Activity activity) { + server.setServerStatusCallback(null); + activity.unbindService(this); + } + + public boolean connected() { + return server != null; + } + + public @Nullable ServerService getServer() { + return server; + } + + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + server = ((ServerService.ServerBinder) service).getService(); + server.setServerStatusCallback(this.callback); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + server = null; + } + } +}
\ No newline at end of file diff --git a/app/src/main/java/com/javispedro/vndroid/SetupActivity.java b/app/src/main/java/com/javispedro/vndroid/SetupActivity.java deleted file mode 100644 index 123bbe7..0000000 --- a/app/src/main/java/com/javispedro/vndroid/SetupActivity.java +++ /dev/null @@ -1,78 +0,0 @@ -package com.javispedro.vndroid; - -import android.app.Activity; -import android.content.Intent; -import android.media.projection.MediaProjectionManager; -import android.os.Bundle; -import android.support.v7.app.AppCompatActivity; -import android.util.Log; -import android.view.View; - -public class SetupActivity extends AppCompatActivity { - private static String TAG = SetupActivity.class.getSimpleName(); - - private static final int REQUEST_MEDIA_PROJECTION = 1; - - private boolean mirror = true; - - @Override - protected void onCreate(Bundle savedInstanceState) { - super.onCreate(savedInstanceState); - setContentView(R.layout.activity_setup); - } - - @Override - public void onActivityResult(int requestCode, int resultCode, Intent data) - { - switch (requestCode) { - case REQUEST_MEDIA_PROJECTION: - if (resultCode != Activity.RESULT_OK) { - Log.w(TAG, "User cancelled media projection"); - return; - } - - notifyMediaProjectionResult(resultCode, data); - startServer(); - - break; - } - } - - private void startServer() - { - Intent intent = new Intent(this, ServerService.class); - intent.setAction(ServerService.ACTION_START); - startService(intent); - } - - private void stopServer() - { - Intent intent = new Intent(this, ServerService.class); - intent.setAction(ServerService.ACTION_STOP); - startService(intent); - } - - private void notifyMediaProjectionResult(int resultCode, Intent resultData) - { - Intent intent = new Intent(this, ServerService.class); - intent.setAction(ServerService.ACTION_NOTIFY_MEDIA_PROJECTION_RESULT); - intent.putExtra("resultCode", resultCode); - intent.putExtra("resultData", resultData); - startService(intent); - } - - public void onStartClick(View view) { - Log.d(TAG, "onStartClick"); - if (mirror) { - MediaProjectionManager manager = getSystemService(MediaProjectionManager.class); - startActivityForResult(manager.createScreenCaptureIntent(), REQUEST_MEDIA_PROJECTION); - } else { - startServer(); - } - } - - public void onStopClick(View view) { - Log.d(TAG, "onStopClick"); - stopServer(); - } -} diff --git a/app/src/main/res/layout/activity_setup.xml b/app/src/main/res/layout/activity_setup.xml deleted file mode 100644 index 9512149..0000000 --- a/app/src/main/res/layout/activity_setup.xml +++ /dev/null @@ -1,45 +0,0 @@ -<?xml version="1.0" encoding="utf-8"?> -<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" - xmlns:app="http://schemas.android.com/apk/res-auto" - xmlns:tools="http://schemas.android.com/tools" - android:layout_width="match_parent" - android:layout_height="match_parent" - tools:context=".SetupActivity"> - - <LinearLayout - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:layout_marginStart="8dp" - android:layout_marginTop="8dp" - android:layout_marginEnd="8dp" - android:layout_marginBottom="8dp" - android:gravity="center" - android:orientation="horizontal" - app:layout_constraintBottom_toBottomOf="parent" - app:layout_constraintEnd_toEndOf="parent" - app:layout_constraintStart_toStartOf="parent" - app:layout_constraintTop_toTopOf="parent" - app:layout_constraintVertical_bias="0.19"> - - <Button - android:id="@+id/btnStart" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:onClick="onStartClick" - android:text="@string/action_start" /> - - <Space - android:layout_width="75dp" - android:layout_height="wrap_content" - android:layout_weight="1" /> - - <Button - android:id="@+id/btnStop" - android:layout_width="wrap_content" - android:layout_height="wrap_content" - android:onClick="onStopClick" - android:text="@string/action_stop" /> - - </LinearLayout> - -</android.support.constraint.ConstraintLayout>
\ No newline at end of file diff --git a/app/src/main/res/layout/settings_activity.xml b/app/src/main/res/layout/settings_activity.xml new file mode 100644 index 0000000..de6591a --- /dev/null +++ b/app/src/main/res/layout/settings_activity.xml @@ -0,0 +1,9 @@ +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <FrameLayout + android:id="@+id/settings" + android:layout_width="match_parent" + android:layout_height="match_parent" /> +</LinearLayout>
\ No newline at end of file diff --git a/app/src/main/res/values/arrays.xml b/app/src/main/res/values/arrays.xml new file mode 100644 index 0000000..6b30d8d --- /dev/null +++ b/app/src/main/res/values/arrays.xml @@ -0,0 +1,3 @@ +<resources> + +</resources>
\ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index 69b2233..d044e23 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,6 +1,6 @@ <?xml version="1.0" encoding="utf-8"?> <resources> - <color name="colorPrimary">#008577</color> - <color name="colorPrimaryDark">#00574B</color> - <color name="colorAccent">#D81B60</color> + <color name="colorPrimary">#2196F3</color> + <color name="colorPrimaryDark">#3F51B5</color> + <color name="colorAccent">#00BCD4</color> </resources> diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml new file mode 100644 index 0000000..8542005 --- /dev/null +++ b/app/src/main/res/values/dimens.xml @@ -0,0 +1,2 @@ +<resources> +</resources> diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 9c0ca47..bf12595 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -7,5 +7,18 @@ <string name="action_start">Start</string> <string name="action_stop">Stop</string> + <string name="toast_no_mirror_permission">Cannot continue without broadcast screen permission</string> <string name="toast_no_input_service">Accessibility service not enabled: clients will not be able to control</string> + + <!-- Settings activity --> + <string name="settings_category_main">Main</string> + <string name="settings_enable_key" translatable="false">enable</string> + <string name="settings_enable_title">Enable server</string> + <string name="settings_status_key" translatable="false">status</string> + <string name="status_server_enabled">Server enabled</string> + <string name="status_server_addresses">Server addresses:</string> + <plurals name="status_num_clients"> + <item quantity="one">%d client connected.</item> + <item quantity="other">%d clients connected.</item> + </plurals> </resources> diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml index 5885930..545b9c6 100644 --- a/app/src/main/res/values/styles.xml +++ b/app/src/main/res/values/styles.xml @@ -8,4 +8,13 @@ <item name="colorAccent">@color/colorAccent</item> </style> + <style name="AppTheme.NoActionBar"> + <item name="windowActionBar">false</item> + <item name="windowNoTitle">true</item> + </style> + + <style name="AppTheme.AppBarOverlay" parent="ThemeOverlay.AppCompat.Dark.ActionBar" /> + + <style name="AppTheme.PopupOverlay" parent="ThemeOverlay.AppCompat.Light" /> + </resources> diff --git a/app/src/main/res/xml/root_preferences.xml b/app/src/main/res/xml/root_preferences.xml new file mode 100644 index 0000000..daedcc8 --- /dev/null +++ b/app/src/main/res/xml/root_preferences.xml @@ -0,0 +1,31 @@ +<!-- + ~ Copyright 2018 The app Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<PreferenceScreen xmlns:app="http://schemas.android.com/apk/res-auto"> + + <PreferenceCategory app:title="@string/settings_category_main"> + <SwitchPreferenceCompat + app:key="@string/settings_enable_key" + app:title="@string/settings_enable_title" + app:persistent="false" /> + + <Preference + app:key="status" + app:title="" + app:persistent="false"/> + </PreferenceCategory> + +</PreferenceScreen> diff --git a/gradle.properties b/gradle.properties index 82618ce..f6853fb 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,5 +11,9 @@ org.gradle.jvmargs=-Xmx1536m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true - - +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true +# Automatically convert third-party libraries to use AndroidX +android.enableJetifier=true
\ No newline at end of file |