summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJavier <dev.git@javispedro.com>2020-02-16 22:18:38 +0100
committerJavier <dev.git@javispedro.com>2020-02-16 22:18:38 +0100
commit0a72437088b3e8387aa6ab77e20293bc2385788a (patch)
treed9add727dee224eda5d4d4823a08ce14e37f107f
parente88fb30dd01e64a0b2bfd1baec949b1836772fc0 (diff)
downloadvndroid-0a72437088b3e8387aa6ab77e20293bc2385788a.tar.gz
vndroid-0a72437088b3e8387aa6ab77e20293bc2385788a.zip
update to sdk 29, migrate to androidx, new GUI
-rw-r--r--.gitignore5
-rw-r--r--.idea/codeStyles/Project.xml116
-rw-r--r--.idea/gradle.xml16
-rw-r--r--.idea/misc.xml9
-rw-r--r--.idea/runConfigurations.xml12
-rw-r--r--.idea/vcs.xml6
-rw-r--r--app/build.gradle18
-rw-r--r--app/libs/.gitignore3
-rw-r--r--app/src/main/AndroidManifest.xml10
-rw-r--r--app/src/main/cpp/native-lib.cpp50
-rw-r--r--app/src/main/java/com/javispedro/vndroid/ControlService.java3
-rw-r--r--app/src/main/java/com/javispedro/vndroid/KeyEventOutput.java3
-rw-r--r--app/src/main/java/com/javispedro/vndroid/RFBServer.java15
-rw-r--r--app/src/main/java/com/javispedro/vndroid/ScreenGrabber.java3
-rw-r--r--app/src/main/java/com/javispedro/vndroid/ScreenMirrorGrabber.java36
-rw-r--r--app/src/main/java/com/javispedro/vndroid/ServerRunningNotification.java12
-rw-r--r--app/src/main/java/com/javispedro/vndroid/ServerService.java230
-rw-r--r--app/src/main/java/com/javispedro/vndroid/SettingsActivity.java213
-rw-r--r--app/src/main/java/com/javispedro/vndroid/SetupActivity.java78
-rw-r--r--app/src/main/res/layout/activity_setup.xml45
-rw-r--r--app/src/main/res/layout/settings_activity.xml9
-rw-r--r--app/src/main/res/values/arrays.xml3
-rw-r--r--app/src/main/res/values/colors.xml6
-rw-r--r--app/src/main/res/values/dimens.xml2
-rw-r--r--app/src/main/res/values/strings.xml13
-rw-r--r--app/src/main/res/values/styles.xml9
-rw-r--r--app/src/main/res/xml/root_preferences.xml31
-rw-r--r--gradle.properties8
28 files changed, 723 insertions, 241 deletions
diff --git a/.gitignore b/.gitignore
index fd45b12..603b140 100644
--- a/.gitignore
+++ b/.gitignore
@@ -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