diff --git a/.gitignore b/.gitignore
index f6b286ce..4fe2129c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -34,7 +34,7 @@ captures/
# Intellij
*.iml
-.idea/workspace.xml
+.idea
# Keystore files
*.jks
diff --git a/app/.gitignore b/app/.gitignore
new file mode 100644
index 00000000..a2def884
--- /dev/null
+++ b/app/.gitignore
@@ -0,0 +1,2 @@
+/build
+google-services.json
diff --git a/app/build.gradle b/app/build.gradle
new file mode 100644
index 00000000..00090951
--- /dev/null
+++ b/app/build.gradle
@@ -0,0 +1,42 @@
+apply plugin: 'com.android.application'
+
+
+android {
+ signingConfigs {
+
+ }
+ compileSdkVersion 24
+ buildToolsVersion "24.0.0"
+ defaultConfig {
+ applicationId "info.dvkr.screenstream"
+ minSdkVersion 21
+ targetSdkVersion 24
+ versionCode 1
+ versionName "1.0"
+
+ resConfigs "en", "ru"
+ }
+ buildTypes {
+ release {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ debug {
+ minifyEnabled true
+ shrinkResources true
+ proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
+ }
+ }
+ aaptOptions {
+ cruncherEnabled = false
+ }
+}
+
+dependencies {
+ compile fileTree(include: ['*.jar'], dir: 'libs')
+ compile 'com.android.support:appcompat-v7:24.0.0'
+ compile 'com.google.firebase:firebase-crash:9.2.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
new file mode 100644
index 00000000..41afecb1
--- /dev/null
+++ b/app/proguard-rules.pro
@@ -0,0 +1,17 @@
+# Add project specific ProGuard rules here.
+# By default, the flags in this file are appended to flags specified
+# in D:\Android\sdk/tools/proguard/proguard-android.txt
+# You can edit the include path and order by changing the proguardFiles
+# directive in build.gradle.
+#
+# For more details, see
+# http://developer.android.com/guide/developing/tools/proguard.html
+
+# Add any project specific keep options here:
+
+# If your project uses WebView with JS, uncomment the following
+# and specify the fully qualified class name to the JavaScript interface
+# class:
+#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
+# public *;
+#}
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
new file mode 100644
index 00000000..cf7e2753
--- /dev/null
+++ b/app/src/main/AndroidManifest.xml
@@ -0,0 +1,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/assets/favicon.png b/app/src/main/assets/favicon.png
new file mode 100644
index 00000000..bcaf68b5
Binary files /dev/null and b/app/src/main/assets/favicon.png differ
diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html
new file mode 100644
index 00000000..40d2b910
--- /dev/null
+++ b/app/src/main/assets/index.html
@@ -0,0 +1,11 @@
+
+
+
+
+ Screen Stream
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/ic_launcher-web.png b/app/src/main/ic_launcher-web.png
new file mode 100644
index 00000000..f6a4cdd3
Binary files /dev/null and b/app/src/main/ic_launcher-web.png differ
diff --git a/app/src/main/java/info/dvkr/screenstream/ApplicationContext.java b/app/src/main/java/info/dvkr/screenstream/ApplicationContext.java
new file mode 100644
index 00000000..f9b5ba25
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/ApplicationContext.java
@@ -0,0 +1,167 @@
+package info.dvkr.screenstream;
+
+import android.app.Application;
+import android.content.Context;
+import android.content.Intent;
+import android.graphics.Point;
+import android.media.projection.MediaProjection;
+import android.media.projection.MediaProjectionManager;
+import android.net.wifi.WifiManager;
+import android.util.DisplayMetrics;
+import android.view.WindowManager;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.util.Locale;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.LinkedTransferQueue;
+
+
+public class ApplicationContext extends Application {
+ private static ApplicationContext instance;
+
+ private ApplicationSettings applicationSettings;
+ private WindowManager windowManager;
+ private MediaProjectionManager projectionManager;
+ private MediaProjection mediaProjection;
+ private int densityDPI;
+ private String indexHtmlPage;
+ private byte[] iconBytes;
+
+ private final LinkedTransferQueue JPEGQueue = new LinkedTransferQueue<>();
+ private final ConcurrentLinkedQueue clientQueue = new ConcurrentLinkedQueue<>();
+
+ private volatile boolean isStreamRunning;
+ private volatile boolean isForegroundServiceRunning;
+
+ @Override
+ public void onCreate() {
+ super.onCreate();
+ instance = this;
+
+ applicationSettings = new ApplicationSettings(this);
+
+ windowManager = (WindowManager) getSystemService(Context.WINDOW_SERVICE);
+ projectionManager = (MediaProjectionManager) getSystemService(Context.MEDIA_PROJECTION_SERVICE);
+ densityDPI = getDensityDPI();
+ indexHtmlPage = getIndexHTML();
+ setFavicon();
+ }
+
+ static ApplicationSettings getApplicationSettings() {
+ return instance.applicationSettings;
+ }
+
+ static WindowManager getWindowsManager() {
+ return instance.windowManager;
+ }
+
+ static MediaProjectionManager getProjectionManager() {
+ return instance.projectionManager;
+ }
+
+ static void setMediaProjection(final int resultCode, final Intent data) {
+ instance.mediaProjection = instance.projectionManager.getMediaProjection(resultCode, data);
+ }
+
+ static MediaProjection getMediaProjection() {
+ return instance.mediaProjection;
+ }
+
+ static int getScreenDensity() {
+ return instance.densityDPI;
+ }
+
+ static float getScale() {
+ return instance.getResources().getDisplayMetrics().density;
+ }
+
+ static Point getScreenSize() {
+ final Point screenSize = new Point();
+ instance.windowManager.getDefaultDisplay().getRealSize(screenSize);
+ return screenSize;
+ }
+
+ static boolean isStreamRunning() {
+ return instance.isStreamRunning;
+ }
+
+ static void setIsStreamRunning(final boolean isRunning) {
+ instance.isStreamRunning = isRunning;
+ }
+
+ static boolean isForegroundServiceRunning() {
+ return instance.isForegroundServiceRunning;
+ }
+
+ static void setIsForegroundServiceRunning(final boolean isRunning) {
+ instance.isForegroundServiceRunning = isRunning;
+ }
+
+ static String getIndexHtmlPage() {
+ return instance.indexHtmlPage;
+ }
+
+ static byte[] getIconBytes() {
+ return instance.iconBytes;
+ }
+
+ static String getServerAddress() {
+ return "http://" + instance.getIPAddress() + ":" + instance.applicationSettings.getSeverPort();
+ }
+
+ static LinkedTransferQueue getJPEGQueue() {
+ return instance.JPEGQueue;
+ }
+
+ static ConcurrentLinkedQueue getClientQueue() {
+ return instance.clientQueue;
+ }
+
+ static boolean isWiFIConnected() {
+ final WifiManager wifi = (WifiManager) instance.getSystemService(Context.WIFI_SERVICE);
+ return wifi.getConnectionInfo().getNetworkId() != -1;
+ }
+
+ // Private methods
+ private String getIPAddress() {
+ final int ipInt = ((WifiManager) getSystemService(Context.WIFI_SERVICE)).getConnectionInfo().getIpAddress();
+ return String.format(Locale.US, "%d.%d.%d.%d", (ipInt & 0xff), (ipInt >> 8 & 0xff), (ipInt >> 16 & 0xff), (ipInt >> 24 & 0xff));
+ }
+
+ private int getDensityDPI() {
+ final DisplayMetrics displayMetrics = new DisplayMetrics();
+ windowManager.getDefaultDisplay().getMetrics(displayMetrics);
+ return displayMetrics.densityDpi;
+ }
+
+ private String getIndexHTML() {
+ final StringBuilder sb = new StringBuilder();
+ String line;
+ try (BufferedReader reader =
+ new BufferedReader(
+ new InputStreamReader(getAssets().open("index.html"), "UTF-8")
+ )) {
+ while ((line = reader.readLine()) != null) sb.append(line.toCharArray());
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ final String html = sb.toString();
+ sb.setLength(0);
+ return html;
+ }
+
+ private void setFavicon() {
+ try (InputStream inputStream = getAssets().open("favicon.png")) {
+ iconBytes = new byte[inputStream.available()];
+ inputStream.read(iconBytes);
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ }
+}
+
diff --git a/app/src/main/java/info/dvkr/screenstream/ApplicationSettings.java b/app/src/main/java/info/dvkr/screenstream/ApplicationSettings.java
new file mode 100644
index 00000000..c02be758
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/ApplicationSettings.java
@@ -0,0 +1,66 @@
+package info.dvkr.screenstream;
+
+
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.preference.PreferenceManager;
+
+final class ApplicationSettings {
+ private static final String DEFAULT_SERVER_PORT = "8080";
+ private static final String DEFAULT_JPEG_QUALITY = "80";
+ private static final String DEFAULT_CLIENT_TIMEOUT = "3000";
+
+ private final SharedPreferences sharedPreferences;
+
+ private boolean minimizeOnStream;
+ private boolean pauseOnSleep;
+ private volatile int severPort;
+ private volatile int jpegQuality;
+ private volatile int clientTimeout;
+
+ ApplicationSettings(Context context) {
+ sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
+
+ minimizeOnStream = sharedPreferences.getBoolean("minimize_on_stream", true);
+ pauseOnSleep = sharedPreferences.getBoolean("pause_on_sleep", false);
+ severPort = Integer.parseInt(sharedPreferences.getString("port_number", DEFAULT_SERVER_PORT));
+ jpegQuality = Integer.parseInt(sharedPreferences.getString("jpeg_quality", DEFAULT_JPEG_QUALITY));
+ clientTimeout = Integer.parseInt(sharedPreferences.getString("client_connection_timeout", DEFAULT_CLIENT_TIMEOUT));
+ }
+
+ boolean updateSettings() {
+ minimizeOnStream = sharedPreferences.getBoolean("minimize_on_stream", true);
+ pauseOnSleep = sharedPreferences.getBoolean("pause_on_sleep", false);
+
+ jpegQuality = Integer.parseInt(sharedPreferences.getString("jpeg_quality", DEFAULT_JPEG_QUALITY));
+ clientTimeout = Integer.parseInt(sharedPreferences.getString("client_connection_timeout", DEFAULT_CLIENT_TIMEOUT));
+
+ final int newSeverPort = Integer.parseInt(sharedPreferences.getString("port_number", DEFAULT_SERVER_PORT));
+ if (newSeverPort != severPort) {
+ severPort = newSeverPort;
+ return true;
+ }
+
+ return false;
+ }
+
+ boolean isMinimizeOnStream() {
+ return minimizeOnStream;
+ }
+
+ boolean isPauseOnSleep() {
+ return pauseOnSleep;
+ }
+
+ int getSeverPort() {
+ return severPort;
+ }
+
+ int getJpegQuality() {
+ return jpegQuality;
+ }
+
+ int getClientTimeout() {
+ return clientTimeout;
+ }
+}
diff --git a/app/src/main/java/info/dvkr/screenstream/Client.java b/app/src/main/java/info/dvkr/screenstream/Client.java
new file mode 100644
index 00000000..b5a24082
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/Client.java
@@ -0,0 +1,60 @@
+package info.dvkr.screenstream;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.net.Socket;
+import java.util.concurrent.Callable;
+
+final class Client implements Callable {
+ private final Socket socket;
+ private final OutputStreamWriter outputStreamWriter;
+ private byte[] jpegImage;
+
+ Client(final Socket socket) throws IOException {
+ this.socket = socket;
+ outputStreamWriter = new OutputStreamWriter(socket.getOutputStream());
+ }
+
+ void closeSocket() {
+ try {
+ outputStreamWriter.close();
+ socket.close();
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ }
+
+ String getClientAddress() {
+ return socket.getInetAddress() + ":" + socket.getPort();
+ }
+
+ void sendHeader() throws IOException {
+ outputStreamWriter.write("HTTP/1.1 200 OK\r\n");
+ outputStreamWriter.write("Content-Type: multipart/x-mixed-replace; boundary=y5exa7CYPPqoASFONZJMz4Ky\r\n");
+ outputStreamWriter.write("Cache-Control: no-store, no-cache, must-revalidate, pre-check=0, post-check=0, max-age=0\r\n");
+ outputStreamWriter.write("Pragma: no-cache\r\n");
+ outputStreamWriter.write("Connection: keep-alive\r\n");
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.flush();
+ }
+
+ void registerImage(final byte[] newJpegImage) {
+ jpegImage = newJpegImage;
+ }
+
+ @Override
+ public Object call() throws Exception {
+ outputStreamWriter.write("--y5exa7CYPPqoASFONZJMz4Ky\r\n");
+ outputStreamWriter.write("Content-Type: image/jpeg\r\n");
+ outputStreamWriter.write("Content-Length: " + jpegImage.length + "\r\n");
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.flush();
+ socket.getOutputStream().write(jpegImage);
+ socket.getOutputStream().flush();
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.flush();
+ return null;
+ }
+}
diff --git a/app/src/main/java/info/dvkr/screenstream/ForegroundService.java b/app/src/main/java/info/dvkr/screenstream/ForegroundService.java
new file mode 100644
index 00000000..263c5604
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/ForegroundService.java
@@ -0,0 +1,262 @@
+package info.dvkr.screenstream;
+
+import android.app.Notification;
+import android.app.PendingIntent;
+import android.app.Service;
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.net.wifi.WifiManager;
+import android.os.HandlerThread;
+import android.os.IBinder;
+import android.os.Process;
+import android.support.annotation.Nullable;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.content.ContextCompat;
+
+public final class ForegroundService extends Service {
+ private static ForegroundService foregroundService;
+
+ // Fields for streaming
+ private HTTPServer httpServer;
+ private ImageGenerator imageGenerator;
+ private TaskHandler foregroundServiceTaskHandler;
+
+ // Fields for broadcast
+ static final String SERVICE_ACTION = "info.dvkr.screenstream.ForegroundService.SERVICE_ACTION";
+
+ static final String SERVICE_PERMISSION = "info.dvkr.screenstream.RECEIVE_BROADCAST";
+ static final String SERVICE_MESSAGE = "SERVICE_MESSAGE";
+ static final int SERVICE_MESSAGE_GET_STATUS = 1000;
+ static final int SERVICE_MESSAGE_UPDATE_STATUS = 1005;
+ static final int SERVICE_MESSAGE_PREPARE_STREAMING = 1010;
+ static final int SERVICE_MESSAGE_START_STREAMING = 1020;
+ static final int SERVICE_MESSAGE_STOP_STREAMING = 1030;
+ static final int SERVICE_MESSAGE_RESTART_HTTP = 1040;
+ static final int SERVICE_MESSAGE_EXIT = 1100;
+
+ static final String SERVICE_MESSAGE_CLIENTS_COUNT = "SERVICE_MESSAGE_CLIENTS_COUNT";
+ static final int SERVICE_MESSAGE_GET_CLIENT_COUNT = 1040;
+ static final String SERVICE_MESSAGE_SERVER_ADDRESS = "SERVICE_MESSAGE_SERVER_ADDRESS";
+ static final int SERVICE_MESSAGE_GET_SERVER_ADDRESS = 1050;
+
+ private int currentServiceMessage;
+
+ // Fields for notifications
+ private Notification startNotification;
+ private BroadcastReceiver localNotificationReceiver;
+ private final String KEY_START = "info.dvkr.screenstream.ForegroundService.startStream";
+ private final Intent startStreamIntent = new Intent(KEY_START);
+ private final String KEY_STOP = "info.dvkr.screenstream.ForegroundService.stopStream";
+ private final Intent stopStreamIntent = new Intent(KEY_STOP);
+ private final String KEY_CLOSE = "info.dvkr.screenstream.ForegroundService.closeService";
+ private final Intent closeIntent = new Intent(KEY_CLOSE);
+
+ private BroadcastReceiver broadcastReceiver;
+
+ @Override
+ public void onCreate() {
+ foregroundService = this;
+
+ httpServer = new HTTPServer();
+ imageGenerator = new ImageGenerator(
+ getResources().getString(R.string.press),
+ getResources().getString(R.string.start_stream),
+ getResources().getString(R.string.on_device)
+ );
+
+ // Starting thread Handler
+ final HandlerThread looperThread = new HandlerThread("ForegroundServiceHandlerThread", Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ looperThread.start();
+ foregroundServiceTaskHandler = new TaskHandler(looperThread.getLooper());
+
+ //Local notifications
+ startNotification = getNotificationStart();
+
+ final IntentFilter localNotificationIntentFilter = new IntentFilter();
+ localNotificationIntentFilter.addAction(KEY_START);
+ localNotificationIntentFilter.addAction(KEY_STOP);
+ localNotificationIntentFilter.addAction(KEY_CLOSE);
+
+ localNotificationReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(KEY_START)) {
+ currentServiceMessage = SERVICE_MESSAGE_START_STREAMING;
+ relayMessageViaActivity();
+ }
+
+ if (intent.getAction().equals(KEY_STOP)) {
+ currentServiceMessage = SERVICE_MESSAGE_STOP_STREAMING;
+ relayMessageViaActivity();
+ }
+
+ if (intent.getAction().equals(KEY_CLOSE)) {
+ currentServiceMessage = SERVICE_MESSAGE_EXIT;
+ relayMessageViaActivity();
+ }
+ }
+ };
+
+ registerReceiver(localNotificationReceiver, localNotificationIntentFilter);
+
+ // Registering receiver for screen off messages
+ final IntentFilter screenOnOffFilter = new IntentFilter();
+ screenOnOffFilter.addAction(Intent.ACTION_SCREEN_OFF);
+ screenOnOffFilter.addAction(WifiManager.NETWORK_STATE_CHANGED_ACTION);
+
+ broadcastReceiver = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (ApplicationContext.getApplicationSettings().isPauseOnSleep())
+ if (intent.getAction().equals(Intent.ACTION_SCREEN_OFF))
+ if (ApplicationContext.isStreamRunning()) {
+ currentServiceMessage = SERVICE_MESSAGE_STOP_STREAMING;
+ relayMessageViaActivity();
+ }
+
+ if (intent.getAction().equals(WifiManager.NETWORK_STATE_CHANGED_ACTION)) {
+ currentServiceMessage = SERVICE_MESSAGE_GET_STATUS;
+ sendBroadcast(new Intent(SERVICE_ACTION).putExtra(SERVICE_MESSAGE, SERVICE_MESSAGE_UPDATE_STATUS), SERVICE_PERMISSION);
+ }
+ }
+ };
+
+ registerReceiver(broadcastReceiver, screenOnOffFilter);
+
+ sendBroadcast(new Intent(SERVICE_ACTION).putExtra(SERVICE_MESSAGE, SERVICE_MESSAGE_UPDATE_STATUS), SERVICE_PERMISSION);
+
+ imageGenerator.addDefaultScreen();
+ httpServer.start();
+ }
+
+ @Override
+ public int onStartCommand(Intent intent, int flags, int startId) {
+ final int messageFromActivity = intent.getIntExtra(SERVICE_MESSAGE, 0);
+ if (messageFromActivity == 0) return START_NOT_STICKY;
+
+ if (messageFromActivity == SERVICE_MESSAGE_PREPARE_STREAMING) {
+ startForeground(110, startNotification);
+ ApplicationContext.setIsForegroundServiceRunning(true);
+ }
+
+ if (messageFromActivity == SERVICE_MESSAGE_GET_STATUS) {
+ sendCurrentServiceMessage();
+ sendServerAddress();
+ sendClientCount();
+ }
+
+ if (messageFromActivity == SERVICE_MESSAGE_START_STREAMING) {
+ stopForeground(true);
+ foregroundServiceTaskHandler.obtainMessage(TaskHandler.HANDLER_START_STREAMING).sendToTarget();
+ startForeground(120, getNotificationStop());
+ }
+
+ if (messageFromActivity == SERVICE_MESSAGE_STOP_STREAMING) {
+ stopForeground(true);
+ foregroundServiceTaskHandler.obtainMessage(TaskHandler.HANDLER_STOP_STREAMING).sendToTarget();
+ startForeground(110, startNotification);
+
+ imageGenerator.addDefaultScreen();
+ }
+
+ if (messageFromActivity == SERVICE_MESSAGE_RESTART_HTTP) {
+ httpServer.stop();
+ imageGenerator.addDefaultScreen();
+ httpServer.start();
+ }
+
+ return START_NOT_STICKY;
+ }
+
+ @Override
+ public void onDestroy() {
+ httpServer.stop();
+ stopForeground(true);
+ unregisterReceiver(broadcastReceiver);
+ unregisterReceiver(localNotificationReceiver);
+ foregroundServiceTaskHandler.getLooper().quit();
+ }
+
+ private void relayMessageViaActivity() {
+ startActivity(new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK));
+ sendBroadcast(new Intent(SERVICE_ACTION).putExtra(SERVICE_MESSAGE, SERVICE_MESSAGE_UPDATE_STATUS), SERVICE_PERMISSION);
+ }
+
+ // Static methods
+ static ImageGenerator getImageGenerator() {
+ return foregroundService.imageGenerator;
+ }
+
+ static void addClient(final Client client) {
+ ApplicationContext.getClientQueue().add(client);
+ foregroundService.sendClientCount();
+ }
+
+ static void removeClient(final Client client) {
+ ApplicationContext.getClientQueue().remove(client);
+ foregroundService.sendClientCount();
+ }
+
+ static void clearClients() {
+ ApplicationContext.getClientQueue().clear();
+ foregroundService.sendClientCount();
+ }
+
+ // Private methods
+ private Notification getNotificationStart() {
+ final Intent mainActivityIntent = new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ final PendingIntent pendingMainActivityIntent = PendingIntent.getActivity(this, 0, mainActivityIntent, 0);
+
+ final NotificationCompat.Builder startNotificationBuilder = new NotificationCompat.Builder(this);
+ startNotificationBuilder.setSmallIcon(R.drawable.ic_cast_http_24dp);
+ startNotificationBuilder.setColor(ContextCompat.getColor(this, R.color.colorPrimaryDark));
+ startNotificationBuilder.setContentTitle(getResources().getString(R.string.ready_to_stream));
+ startNotificationBuilder.setContentText(getResources().getString(R.string.press_start));
+ startNotificationBuilder.setContentIntent(pendingMainActivityIntent);
+ startNotificationBuilder.addAction(R.drawable.ic_play_arrow_24dp, getResources().getString(R.string.start), PendingIntent.getBroadcast(this, 0, startStreamIntent, 0));
+ startNotificationBuilder.addAction(R.drawable.ic_clear_24dp, getResources().getString(R.string.close), PendingIntent.getBroadcast(this, 0, closeIntent, 0));
+ return startNotificationBuilder.build();
+ }
+
+ private Notification getNotificationStop() {
+ final Intent mainActivityIntent = new Intent(this, MainActivity.class).addFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ final PendingIntent pendingMainActivityIntent = PendingIntent.getActivity(this, 0, mainActivityIntent, 0);
+
+ final NotificationCompat.Builder stopNotificationBuilder = new NotificationCompat.Builder(this);
+ stopNotificationBuilder.setSmallIcon(R.drawable.ic_cast_http_24dp);
+ stopNotificationBuilder.setColor(ContextCompat.getColor(this, R.color.colorPrimaryDark));
+ stopNotificationBuilder.setContentTitle(getResources().getString(R.string.stream));
+ stopNotificationBuilder.setContentText(getResources().getString(R.string.go_to) + ApplicationContext.getServerAddress());
+ stopNotificationBuilder.setContentIntent(pendingMainActivityIntent);
+ stopNotificationBuilder.addAction(R.drawable.ic_stop_24dp, getResources().getString(R.string.stop), PendingIntent.getBroadcast(this, 0, stopStreamIntent, 0));
+ return stopNotificationBuilder.build();
+ }
+
+ private void sendCurrentServiceMessage() {
+ sendBroadcast(new Intent(SERVICE_ACTION).putExtra(SERVICE_MESSAGE, currentServiceMessage),
+ SERVICE_PERMISSION);
+ currentServiceMessage = 0;
+ }
+
+ private void sendClientCount() {
+ sendBroadcast(new Intent(SERVICE_ACTION)
+ .putExtra(SERVICE_MESSAGE, SERVICE_MESSAGE_GET_CLIENT_COUNT)
+ .putExtra(SERVICE_MESSAGE_CLIENTS_COUNT, ApplicationContext.getClientQueue().size()),
+ SERVICE_PERMISSION);
+ }
+
+ private void sendServerAddress() {
+ sendBroadcast(new Intent(SERVICE_ACTION)
+ .putExtra(SERVICE_MESSAGE, SERVICE_MESSAGE_GET_SERVER_ADDRESS)
+ .putExtra(SERVICE_MESSAGE_SERVER_ADDRESS, ApplicationContext.getServerAddress()),
+ SERVICE_PERMISSION);
+ }
+
+ @Nullable
+ @Override
+ public IBinder onBind(Intent intent) {
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/info/dvkr/screenstream/HTTPServer.java b/app/src/main/java/info/dvkr/screenstream/HTTPServer.java
new file mode 100644
index 00000000..7654efa1
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/HTTPServer.java
@@ -0,0 +1,141 @@
+package info.dvkr.screenstream;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.OutputStreamWriter;
+import java.net.ServerSocket;
+import java.net.Socket;
+import java.net.SocketTimeoutException;
+
+final class HTTPServer {
+ private final Object lock = new Object();
+ private static final int SEVER_SOCKET_TIMEOUT = 50;
+
+ private volatile boolean isThreadRunning;
+
+ private ServerSocket serverSocket;
+ private HTTPServerThread httpServerThread;
+ private JpegStreamer jpegStreamer;
+
+ private class HTTPServerThread extends Thread {
+
+
+ HTTPServerThread() {
+ super("HTTPServerThread");
+ }
+
+ public void run() {
+ while (!isInterrupted()) {
+ synchronized (lock) {
+ if (!isThreadRunning) continue;
+ try {
+ final Socket clientSocket = HTTPServer.this.serverSocket.accept();
+ final BufferedReader bufferedReaderFromClient = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
+ final String requestLine = bufferedReaderFromClient.readLine();
+
+ if (requestLine == null) continue;
+
+ final String requestUri = requestLine.split(" ")[1];
+ switch (requestUri) {
+ case "/":
+ sendMainPage(clientSocket);
+ break;
+ case "/screen_stream.mjpeg":
+ HTTPServer.this.jpegStreamer.addClient(clientSocket);
+ break;
+ case "/favicon.ico":
+ sendFavicon(clientSocket);
+ break;
+ default:
+ sendNotFound(clientSocket);
+ }
+ } catch (SocketTimeoutException ex) {
+ // NOOP
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ }
+ } // while
+ } // run()
+
+ private void sendMainPage(final Socket socket) throws IOException {
+ try (final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(socket.getOutputStream())) {
+ outputStreamWriter.write("HTTP/1.1 200 OK\r\n");
+ outputStreamWriter.write("Content-Type: text/html\r\n");
+ outputStreamWriter.write("Connection: close\r\n");
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.write(ApplicationContext.getIndexHtmlPage());
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.flush();
+ }
+ }
+
+ private void sendFavicon(Socket socket) throws IOException {
+ try (final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(socket.getOutputStream())) {
+ outputStreamWriter.write("HTTP/1.1 200 OK\r\n");
+ outputStreamWriter.write("Content-Type: image/png\r\n");
+ outputStreamWriter.write("Connection: close\r\n");
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.flush();
+ socket.getOutputStream().write(ApplicationContext.getIconBytes());
+ socket.getOutputStream().flush();
+ }
+ }
+
+ private void sendNotFound(final Socket socket) throws IOException {
+ try (final OutputStreamWriter outputStreamWriter = new OutputStreamWriter(socket.getOutputStream())) {
+ outputStreamWriter.write("HTTP/1.1 301 Moved Permanently\r\n");
+ outputStreamWriter.write("Location: " + ApplicationContext.getServerAddress() + "\r\n");
+ outputStreamWriter.write("Connection: close\r\n");
+ outputStreamWriter.write("\r\n");
+ outputStreamWriter.flush();
+ }
+ }
+ }
+
+
+ void start() {
+ synchronized (lock) {
+ if (isThreadRunning) return;
+ try {
+ serverSocket = new ServerSocket(ApplicationContext.getApplicationSettings().getSeverPort());
+ serverSocket.setSoTimeout(SEVER_SOCKET_TIMEOUT);
+
+ jpegStreamer = new JpegStreamer();
+ jpegStreamer.start();
+
+ httpServerThread = new HTTPServerThread();
+ httpServerThread.start();
+
+ isThreadRunning = true;
+// Log.d(TAG, "HTTP server started on port: " + ApplicationContext.getApplicationSettings().getSeverPort());
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ }
+ }
+
+ void stop() {
+ synchronized (lock) {
+ if (!isThreadRunning) return;
+ isThreadRunning = false;
+
+ httpServerThread.interrupt();
+ httpServerThread = null;
+
+ jpegStreamer.stop();
+ jpegStreamer = null;
+
+ try {
+ serverSocket.close();
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ serverSocket = null;
+// Log.d(TAG, "HTTP server stopped");
+ }
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/java/info/dvkr/screenstream/ImageGenerator.java b/app/src/main/java/info/dvkr/screenstream/ImageGenerator.java
new file mode 100644
index 00000000..1908f317
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/ImageGenerator.java
@@ -0,0 +1,175 @@
+package info.dvkr.screenstream;
+
+import android.graphics.Bitmap;
+import android.graphics.Canvas;
+import android.graphics.Color;
+import android.graphics.Paint;
+import android.graphics.PixelFormat;
+import android.graphics.Rect;
+import android.hardware.display.DisplayManager;
+import android.hardware.display.VirtualDisplay;
+import android.media.Image;
+import android.media.ImageReader;
+import android.os.Handler;
+import android.os.HandlerThread;
+import android.os.Process;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+
+final class ImageGenerator {
+ private final Object lock = new Object();
+ private final ImageReader.OnImageAvailableListener imageListener;
+
+ private volatile boolean isThreadRunning;
+
+ private HandlerThread imageThread;
+ private Handler imageHandler;
+ private ImageReader imageReader;
+ private VirtualDisplay virtualDisplay;
+
+ private final String defaultText1;
+ private final String defaultText2;
+ private final String defaultText3;
+
+ ImageGenerator(final String defaultText1, final String defaultText2, final String defaultText3) {
+ this.defaultText1 = defaultText1;
+ this.defaultText2 = defaultText2;
+ this.defaultText3 = defaultText3;
+
+ imageListener = new ImageReader.OnImageAvailableListener() {
+ @Override
+ public void onImageAvailable(ImageReader imageReader) {
+ synchronized (lock) {
+ if (!isThreadRunning) return;
+
+ final Image image = imageReader.acquireLatestImage();
+ if (image == null) return;
+
+ final Image.Plane[] planes = image.getPlanes();
+ final ByteBuffer bytebuffer = planes[0].getBuffer();
+ bytebuffer.rewind();
+ final int width = planes[0].getRowStride() / planes[0].getPixelStride();
+
+ Bitmap bitmap = Bitmap.createBitmap(width, image.getHeight(), Bitmap.Config.ARGB_8888);
+ bitmap.copyPixelsFromBuffer(bytebuffer);
+
+ if (width > image.getWidth()) {
+ bitmap = Bitmap.createBitmap(bitmap, 0, 0, image.getWidth(), image.getHeight());
+ }
+
+ byte[] jpegByteArray;
+ try (final ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream()) {
+ bitmap.compress(Bitmap.CompressFormat.JPEG, ApplicationContext.getApplicationSettings().getJpegQuality(), jpegOutputStream);
+ jpegOutputStream.flush();
+ jpegByteArray = jpegOutputStream.toByteArray();
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ return;
+ }
+
+ image.close();
+
+ if (jpegByteArray != null) ApplicationContext.getJPEGQueue().add(jpegByteArray);
+ }
+ }
+ };
+
+ }
+
+ void start() {
+ synchronized (lock) {
+ if (isThreadRunning) return;
+
+ imageThread = new HandlerThread("Image capture thread", Process.THREAD_PRIORITY_MORE_FAVORABLE);
+ imageThread.start();
+ imageReader = ImageReader.newInstance(ApplicationContext.getScreenSize().x, ApplicationContext.getScreenSize().y, PixelFormat.RGBA_8888, 2);
+ imageHandler = new Handler(imageThread.getLooper());
+ imageReader.setOnImageAvailableListener(imageListener, imageHandler);
+ virtualDisplay = ApplicationContext.getMediaProjection().createVirtualDisplay(
+ "Screen Stream Virtual Display",
+ ApplicationContext.getScreenSize().x,
+ ApplicationContext.getScreenSize().y,
+ ApplicationContext.getScreenDensity(),
+ DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR,
+ imageReader.getSurface(),
+ null, null);
+
+ isThreadRunning = true;
+// Log.d(TAG, "Image generator started.");
+ }
+ }
+
+ void stop() {
+ synchronized (lock) {
+ if (!isThreadRunning) return;
+ isThreadRunning = false;
+
+ imageReader.setOnImageAvailableListener(null, null);
+ imageReader.close();
+ imageReader = null;
+
+ virtualDisplay.release();
+ virtualDisplay = null;
+
+ imageHandler.removeCallbacksAndMessages(null);
+ imageThread.quit();
+ imageThread = null;
+// Log.d(TAG, "Image generator stopped.");
+ }
+ }
+
+ void addDefaultScreen() {
+ ApplicationContext.getJPEGQueue().clear();
+ new Handler().postDelayed(new Runnable() {
+ @Override
+ public void run() {
+ final Bitmap bitmap = Bitmap.createBitmap(ApplicationContext.getScreenSize().x, ApplicationContext.getScreenSize().y, Bitmap.Config.ARGB_8888);
+ final Canvas canvas = new Canvas(bitmap);
+ canvas.drawRGB(255, 255, 255);
+
+ final Rect bounds = new Rect();
+ final Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG);
+ int textSize = (int) (12 * ApplicationContext.getScale());
+ paint.setTextSize(textSize);
+ paint.setColor(Color.BLACK);
+ paint.getTextBounds(defaultText1, 0, defaultText1.length(), bounds);
+ int x = (bitmap.getWidth() - bounds.width()) / 2;
+ int y = (bitmap.getHeight() + bounds.height()) / 2 - 2 * textSize;
+ canvas.drawText(defaultText1, x, y, paint);
+
+ textSize = (int) (16 * ApplicationContext.getScale());
+ paint.setTextSize(textSize);
+ paint.setColor(Color.rgb(153, 50, 0));
+ paint.getTextBounds(defaultText2.toUpperCase(), 0, defaultText2.length(), bounds);
+ x = (bitmap.getWidth() - bounds.width()) / 2;
+ y = (bitmap.getHeight() + bounds.height()) / 2;
+ canvas.drawText(defaultText2.toUpperCase(), x, y, paint);
+
+ textSize = (int) (12 * ApplicationContext.getScale());
+ paint.setTextSize(textSize);
+ paint.setColor(Color.BLACK);
+ paint.getTextBounds(defaultText3, 0, defaultText3.length(), bounds);
+ x = (bitmap.getWidth() - bounds.width()) / 2;
+ y = (bitmap.getHeight() + bounds.height()) / 2 + 2 * textSize;
+ canvas.drawText(defaultText3, x, y, paint);
+
+ byte[] jpegByteArray = null;
+ try (final ByteArrayOutputStream jpegOutputStream = new ByteArrayOutputStream()) {
+ bitmap.compress(Bitmap.CompressFormat.JPEG, ApplicationContext.getApplicationSettings().getJpegQuality(), jpegOutputStream);
+ jpegOutputStream.flush();
+ jpegByteArray = jpegOutputStream.toByteArray();
+ } catch (IOException e) {
+ FirebaseCrash.report(e);
+ }
+ if (jpegByteArray != null) ApplicationContext.getJPEGQueue().add(jpegByteArray);
+ }
+ }, 500);
+
+ }
+
+}
+
diff --git a/app/src/main/java/info/dvkr/screenstream/JpegStreamer.java b/app/src/main/java/info/dvkr/screenstream/JpegStreamer.java
new file mode 100644
index 00000000..30df4e94
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/JpegStreamer.java
@@ -0,0 +1,114 @@
+package info.dvkr.screenstream;
+
+import android.util.Log;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+import java.io.IOException;
+import java.net.Socket;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+final class JpegStreamer {
+ private static final String TAG = JpegStreamer.class.getSimpleName();
+ private final Object lock = new Object();
+ private final ExecutorService threadPool = Executors.newFixedThreadPool(2);
+
+ private JpegStreamerThread jpegStreamerThread;
+ private volatile boolean isThreadRunning;
+
+ private class JpegStreamerThread extends Thread {
+ private byte[] currentJPEG;
+ private byte[] lastJPEG;
+ private int sleepCount;
+ private Future future;
+
+ JpegStreamerThread() {
+ super("JpegStreamerThread");
+ }
+
+ private void sendLastJPEGToClients() {
+ sleepCount = 0;
+ for (final Client currentClient : ApplicationContext.getClientQueue()) {
+ currentClient.registerImage(lastJPEG);
+ synchronized (lock) {
+ if (!isThreadRunning) return;
+ future = threadPool.submit(currentClient);
+ }
+ try {
+ future.get(ApplicationContext.getApplicationSettings().getClientTimeout(), TimeUnit.MILLISECONDS);
+ } catch (Exception e) {
+ Log.d(TAG, "Remove client: " + currentClient.getClientAddress() + " " + e.toString());
+ FirebaseCrash.report(e);
+ currentClient.closeSocket();
+ ForegroundService.removeClient(currentClient);
+ }
+ }
+ }
+
+ public void run() {
+ while (!isInterrupted()) {
+ if (!isThreadRunning) break;
+ try {
+ currentJPEG = ApplicationContext.getJPEGQueue().poll(16, TimeUnit.MILLISECONDS);
+ } catch (InterruptedException e) {
+ continue;
+ }
+ if (currentJPEG == null) {
+ sleepCount++;
+ if (sleepCount >= 30) sendLastJPEGToClients();
+ } else {
+ lastJPEG = currentJPEG;
+ sendLastJPEGToClients();
+ }
+
+ }
+ }
+
+ } // JpegStreamerThread
+
+ void addClient(final Socket clientSocket) {
+ synchronized (lock) {
+ if (!isThreadRunning) return;
+
+ try {
+ final Client newClient = new Client(clientSocket);
+ newClient.sendHeader();
+ ForegroundService.addClient(newClient);
+// Log.d(TAG, "Added one client: " + newClient.getClientAddress());
+ } catch (IOException e) {
+ //NOOP
+ }
+ }
+ }
+
+ void start() {
+ synchronized (lock) {
+ if (isThreadRunning) return;
+
+ jpegStreamerThread = new JpegStreamerThread();
+ jpegStreamerThread.start();
+
+ isThreadRunning = true;
+// Log.d(TAG, "JPEG Streamer started");
+ }
+ }
+
+ void stop() {
+ synchronized (lock) {
+ if (!isThreadRunning) return;
+ isThreadRunning = false;
+
+ jpegStreamerThread.interrupt();
+ threadPool.shutdownNow();
+
+ for (Client currentClient : ApplicationContext.getClientQueue())
+ currentClient.closeSocket();
+
+ ForegroundService.clearClients();
+// Log.d(TAG, "JPEG Streamer stopped");
+ }
+ }
+}
diff --git a/app/src/main/java/info/dvkr/screenstream/MainActivity.java b/app/src/main/java/info/dvkr/screenstream/MainActivity.java
new file mode 100644
index 00000000..1d2dcded
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/MainActivity.java
@@ -0,0 +1,215 @@
+package info.dvkr.screenstream;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.content.IntentFilter;
+import android.media.projection.MediaProjection;
+import android.os.Bundle;
+import android.support.v7.app.AppCompatActivity;
+import android.view.Menu;
+import android.view.MenuItem;
+import android.view.View;
+import android.widget.TextView;
+import android.widget.Toast;
+import android.widget.ToggleButton;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+public final class MainActivity extends AppCompatActivity {
+ private static final int SCREEN_CAPTURE_REQUEST_CODE = 1;
+ private static final int SETTINGS_REQUEST_CODE = 2;
+
+ private TextView clientsCount;
+ private TextView severAddress;
+ private ToggleButton toggleStream;
+ private MediaProjection.Callback projectionCallback;
+ private BroadcastReceiver broadcastReceiverFromService;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ setContentView(R.layout.activity_main);
+
+ clientsCount = (TextView) findViewById(R.id.clientsCount);
+ severAddress = (TextView) findViewById(R.id.severAddress);
+
+ if (!ApplicationContext.isForegroundServiceRunning()) {
+ final Intent foregroundService = new Intent(this, ForegroundService.class);
+ foregroundService.putExtra(ForegroundService.SERVICE_MESSAGE, ForegroundService.SERVICE_MESSAGE_PREPARE_STREAMING);
+ startService(foregroundService);
+ }
+
+ toggleStream = (ToggleButton) findViewById(R.id.toggleStream);
+ toggleStream.setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(View v) {
+ if (toggleStream.isChecked()) tryStartStreaming();
+ else stopStreaming();
+ }
+ });
+
+ projectionCallback = new MediaProjection.Callback() {
+ @Override
+ public void onStop() {
+ stopStreaming();
+ }
+ };
+
+ // Registering receiver for broadcast messages
+ broadcastReceiverFromService = new BroadcastReceiver() {
+ @Override
+ public void onReceive(Context context, Intent intent) {
+ if (intent.getAction().equals(ForegroundService.SERVICE_ACTION)) {
+ final int serviceMessage = intent.getIntExtra(ForegroundService.SERVICE_MESSAGE, 0);
+
+ // Service ask to update status
+ if (serviceMessage == ForegroundService.SERVICE_MESSAGE_UPDATE_STATUS)
+ updateServiceStatus();
+
+ // Service ask to start streaming
+ if (serviceMessage == ForegroundService.SERVICE_MESSAGE_START_STREAMING)
+ tryStartStreaming();
+
+ // Service ask to stop streaming
+ if (serviceMessage == ForegroundService.SERVICE_MESSAGE_STOP_STREAMING)
+ stopStreaming();
+
+ // Service ask to close application
+ if (serviceMessage == ForegroundService.SERVICE_MESSAGE_EXIT) {
+ stopService(new Intent(MainActivity.this, ForegroundService.class));
+ finish();
+ System.exit(0);
+ }
+
+ // Service ask to update client count
+ if (serviceMessage == ForegroundService.SERVICE_MESSAGE_GET_CLIENT_COUNT) {
+ final String clientCount = String.format(
+ getResources().getString(R.string.connected_clients),
+ intent.getIntExtra(ForegroundService.SERVICE_MESSAGE_CLIENTS_COUNT, 0)
+ );
+ clientsCount.setText(clientCount);
+ }
+
+ // Service ask to update server address
+ if (serviceMessage == ForegroundService.SERVICE_MESSAGE_GET_SERVER_ADDRESS) {
+ if (ApplicationContext.isWiFIConnected()) {
+ severAddress.setText(intent.getStringExtra(ForegroundService.SERVICE_MESSAGE_SERVER_ADDRESS));
+ toggleStream.setEnabled(true);
+ } else {
+ severAddress.setText(getResources().getString(R.string.no_wifi_connected));
+ toggleStream.setEnabled(false);
+ stopStreaming();
+ }
+
+ }
+
+ }
+ }
+ };
+
+ registerReceiver(broadcastReceiverFromService, new IntentFilter(ForegroundService.SERVICE_ACTION), ForegroundService.SERVICE_PERMISSION, null);
+ }
+
+ @Override
+ protected void onStart() {
+ super.onStart();
+ updateServiceStatus();
+ }
+
+ @Override
+ protected void onDestroy() {
+ super.onDestroy();
+ if (ApplicationContext.getMediaProjection() != null)
+ ApplicationContext.getMediaProjection().unregisterCallback(projectionCallback);
+
+ unregisterReceiver(broadcastReceiverFromService);
+ }
+
+ @Override
+ public void onActivityResult(int requestCode, int resultCode, Intent data) {
+ switch (requestCode) {
+ case SCREEN_CAPTURE_REQUEST_CODE:
+ if (resultCode != RESULT_OK) {
+ Toast.makeText(this, getResources().getString(R.string.cast_permission_deny), Toast.LENGTH_SHORT).show();
+ toggleStream.setChecked(false);
+ return;
+ }
+
+ startStreaming(resultCode, data);
+ break;
+ case SETTINGS_REQUEST_CODE:
+ final boolean isServerPortChanged = ApplicationContext.getApplicationSettings().updateSettings();
+ if (isServerPortChanged) restartHTTPServer();
+ break;
+ default:
+ FirebaseCrash.log("Unknown request code: " + requestCode);
+ }
+ }
+
+ @Override
+ public boolean onCreateOptionsMenu(Menu menu) {
+ getMenuInflater().inflate(R.menu.settings, menu);
+ return true;
+ }
+
+ @Override
+ public boolean onOptionsItemSelected(MenuItem item) {
+ switch (item.getItemId()) {
+ case R.id.menu_settings:
+ final Intent intentSettings = new Intent(this, SettingsActivity.class);
+ startActivityForResult(intentSettings, SETTINGS_REQUEST_CODE);
+ return true;
+ default:
+ return super.onOptionsItemSelected(item);
+ }
+ }
+
+ private void updateServiceStatus() {
+ final Intent getStatus = new Intent(this, ForegroundService.class);
+ getStatus.putExtra(ForegroundService.SERVICE_MESSAGE, ForegroundService.SERVICE_MESSAGE_GET_STATUS);
+ startService(getStatus);
+ }
+
+ private void tryStartStreaming() {
+ if (!ApplicationContext.isWiFIConnected()) return;
+ toggleStream.setChecked(true);
+ if (ApplicationContext.isStreamRunning()) return;
+ startActivityForResult(ApplicationContext.getProjectionManager().createScreenCaptureIntent(), SCREEN_CAPTURE_REQUEST_CODE);
+ }
+
+ private void startStreaming(final int resultCode, final Intent data) {
+ ApplicationContext.setMediaProjection(resultCode, data);
+ ApplicationContext.getMediaProjection().registerCallback(projectionCallback, null);
+
+ final Intent startStreaming = new Intent(this, ForegroundService.class);
+ startStreaming.putExtra(ForegroundService.SERVICE_MESSAGE, ForegroundService.SERVICE_MESSAGE_START_STREAMING);
+ startService(startStreaming);
+
+ if (ApplicationContext.getApplicationSettings().isMinimizeOnStream()) {
+ final Intent minimiseMyself = new Intent(Intent.ACTION_MAIN);
+ minimiseMyself.addCategory(Intent.CATEGORY_HOME);
+ minimiseMyself.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
+ startActivity(minimiseMyself);
+ }
+ }
+
+ private void stopStreaming() {
+ toggleStream.setChecked(false);
+ if (!ApplicationContext.isStreamRunning()) return;
+
+ ApplicationContext.getMediaProjection().unregisterCallback(projectionCallback);
+
+ final Intent stopStreaming = new Intent(this, ForegroundService.class);
+ stopStreaming.putExtra(ForegroundService.SERVICE_MESSAGE, ForegroundService.SERVICE_MESSAGE_STOP_STREAMING);
+ startService(stopStreaming);
+ }
+
+ private void restartHTTPServer() {
+ stopStreaming();
+ final Intent resrtartHTTP = new Intent(this, ForegroundService.class);
+ resrtartHTTP.putExtra(ForegroundService.SERVICE_MESSAGE, ForegroundService.SERVICE_MESSAGE_RESTART_HTTP);
+ startService(resrtartHTTP);
+ }
+
+}
\ No newline at end of file
diff --git a/app/src/main/java/info/dvkr/screenstream/SettingsActivity.java b/app/src/main/java/info/dvkr/screenstream/SettingsActivity.java
new file mode 100644
index 00000000..f1e099bd
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/SettingsActivity.java
@@ -0,0 +1,74 @@
+package info.dvkr.screenstream;
+
+
+import android.os.Bundle;
+import android.preference.EditTextPreference;
+import android.preference.ListPreference;
+import android.preference.Preference;
+import android.preference.PreferenceActivity;
+import android.preference.PreferenceFragment;
+import android.widget.Toast;
+
+public final class SettingsActivity extends PreferenceActivity {
+ private static final int minPortNumber = 1025;
+ private static final int maxPortNumber = 65534;
+
+ @Override
+ protected void onCreate(Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ getFragmentManager().beginTransaction().replace(android.R.id.content, new ScreenStreamPreferenceFragment()).commit();
+ }
+
+ public static class ScreenStreamPreferenceFragment extends PreferenceFragment {
+ int index;
+
+ @Override
+ public void onCreate(final Bundle savedInstanceState) {
+ super.onCreate(savedInstanceState);
+ addPreferencesFromResource(R.xml.preferences);
+
+ final String portRange = String.format(getResources().getString(R.string.port_range), minPortNumber, maxPortNumber);
+ final EditTextPreference portNumberTextPreference = (EditTextPreference) findPreference("port_number");
+ portNumberTextPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object data) {
+ final int portNumber = Integer.parseInt((String) data);
+ if ((portNumber < minPortNumber) || (portNumber > maxPortNumber)) {
+ Toast.makeText(getActivity().getApplicationContext(), portRange, Toast.LENGTH_LONG).show();
+ return false;
+ }
+
+ return true;
+ }
+ });
+
+ final ListPreference jpegQualityPreference = (ListPreference) findPreference("jpeg_quality");
+ index = jpegQualityPreference.findIndexOfValue(jpegQualityPreference.getValue());
+ jpegQualityPreference.setSummary(getResources().getString(R.string.settings_jpeg_quality_summary)
+ + getResources().getString(R.string.value) + jpegQualityPreference.getEntries()[index]);
+ jpegQualityPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object data) {
+ int index = jpegQualityPreference.findIndexOfValue(data.toString());
+ jpegQualityPreference.setSummary(getResources().getString(R.string.settings_jpeg_quality_summary)
+ + getResources().getString(R.string.value) + jpegQualityPreference.getEntries()[index]);
+ return true;
+ }
+ });
+
+ final ListPreference clientTimeoutPreference = (ListPreference) findPreference("client_connection_timeout");
+ index = clientTimeoutPreference.findIndexOfValue(clientTimeoutPreference.getValue());
+ clientTimeoutPreference.setSummary(getResources().getString(R.string.client_timeout_summary)
+ + getResources().getString(R.string.value) + clientTimeoutPreference.getEntries()[index]);
+ clientTimeoutPreference.setOnPreferenceChangeListener(new Preference.OnPreferenceChangeListener() {
+ @Override
+ public boolean onPreferenceChange(Preference preference, Object data) {
+ int index = clientTimeoutPreference.findIndexOfValue(data.toString());
+ clientTimeoutPreference.setSummary(getResources().getString(R.string.client_timeout_summary)
+ + getResources().getString(R.string.value) + clientTimeoutPreference.getEntries()[index]);
+ return true;
+ }
+ });
+ }
+ }
+}
diff --git a/app/src/main/java/info/dvkr/screenstream/TaskHandler.java b/app/src/main/java/info/dvkr/screenstream/TaskHandler.java
new file mode 100644
index 00000000..ca060fec
--- /dev/null
+++ b/app/src/main/java/info/dvkr/screenstream/TaskHandler.java
@@ -0,0 +1,75 @@
+package info.dvkr.screenstream;
+
+import android.os.Handler;
+import android.os.Looper;
+import android.os.Message;
+
+import com.google.firebase.crash.FirebaseCrash;
+
+import static android.view.Surface.ROTATION_0;
+import static android.view.Surface.ROTATION_180;
+
+final class TaskHandler extends Handler {
+ static final int HANDLER_START_STREAMING = 0;
+ static final int HANDLER_STOP_STREAMING = 1;
+
+ private static final int HANDLER_PAUSE_STREAMING = 4;
+ private static final int HANDLER_RESUME_STREAMING = 5;
+ private static final int HANDLER_DETECT_ROTATION = 10;
+
+ private int currentOrientation;
+
+ TaskHandler(final Looper looper) {
+ super(looper);
+ }
+
+ @Override
+ public void handleMessage(Message message) {
+ switch (message.what) {
+ case HANDLER_START_STREAMING:
+ if (ApplicationContext.isStreamRunning()) break;
+ removeMessages(HANDLER_DETECT_ROTATION);
+ currentOrientation = getOrientation();
+ ForegroundService.getImageGenerator().start();
+ sendMessageDelayed(obtainMessage(HANDLER_DETECT_ROTATION), 250);
+ ApplicationContext.setIsStreamRunning(true);
+ break;
+ case HANDLER_PAUSE_STREAMING:
+ if (!ApplicationContext.isStreamRunning()) break;
+ ForegroundService.getImageGenerator().stop();
+ sendMessageDelayed(obtainMessage(HANDLER_RESUME_STREAMING), 250);
+ break;
+ case HANDLER_RESUME_STREAMING:
+ if (!ApplicationContext.isStreamRunning()) break;
+ ForegroundService.getImageGenerator().start();
+ sendMessageDelayed(obtainMessage(HANDLER_DETECT_ROTATION), 250);
+ break;
+ case HANDLER_STOP_STREAMING:
+ if (!ApplicationContext.isStreamRunning()) break;
+ removeMessages(HANDLER_DETECT_ROTATION);
+ removeMessages(HANDLER_STOP_STREAMING);
+ ForegroundService.getImageGenerator().stop();
+ ApplicationContext.getMediaProjection().stop();
+ ApplicationContext.setIsStreamRunning(false);
+ break;
+ case HANDLER_DETECT_ROTATION:
+ if (!ApplicationContext.isStreamRunning()) break;
+ final int newOrientation = getOrientation();
+ if (currentOrientation == newOrientation) {
+ sendMessageDelayed(obtainMessage(HANDLER_DETECT_ROTATION), 250);
+ break;
+ }
+ currentOrientation = newOrientation;
+ obtainMessage(HANDLER_PAUSE_STREAMING).sendToTarget();
+ break;
+ default:
+ FirebaseCrash.log("Cannot handle message");
+ }
+ }
+
+ private int getOrientation() {
+ final int rotation = ApplicationContext.getWindowsManager().getDefaultDisplay().getRotation();
+ if (rotation == ROTATION_0 || rotation == ROTATION_180) return 0;
+ return 1;
+ }
+}
\ No newline at end of file
diff --git a/app/src/main/res/drawable/ic_access_time_black_24dp.xml b/app/src/main/res/drawable/ic_access_time_black_24dp.xml
new file mode 100644
index 00000000..2239a4f4
--- /dev/null
+++ b/app/src/main/res/drawable/ic_access_time_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_cast_http_24dp.xml b/app/src/main/res/drawable/ic_cast_http_24dp.xml
new file mode 100644
index 00000000..1be6da95
--- /dev/null
+++ b/app/src/main/res/drawable/ic_cast_http_24dp.xml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
diff --git a/app/src/main/res/drawable/ic_clear_24dp.xml b/app/src/main/res/drawable/ic_clear_24dp.xml
new file mode 100644
index 00000000..1c7c95c3
--- /dev/null
+++ b/app/src/main/res/drawable/ic_clear_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_flip_to_back_black_24dp.xml b/app/src/main/res/drawable/ic_flip_to_back_black_24dp.xml
new file mode 100644
index 00000000..895b4f1b
--- /dev/null
+++ b/app/src/main/res/drawable/ic_flip_to_back_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_high_quality_black_24dp.xml b/app/src/main/res/drawable/ic_high_quality_black_24dp.xml
new file mode 100644
index 00000000..1d683bab
--- /dev/null
+++ b/app/src/main/res/drawable/ic_high_quality_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_http_black_24dp.xml b/app/src/main/res/drawable/ic_http_black_24dp.xml
new file mode 100644
index 00000000..bd4e9152
--- /dev/null
+++ b/app/src/main/res/drawable/ic_http_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_pause_circle_outline_black_24dp.xml b/app/src/main/res/drawable/ic_pause_circle_outline_black_24dp.xml
new file mode 100644
index 00000000..169c05d7
--- /dev/null
+++ b/app/src/main/res/drawable/ic_pause_circle_outline_black_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_play_arrow_24dp.xml b/app/src/main/res/drawable/ic_play_arrow_24dp.xml
new file mode 100644
index 00000000..f9167ce1
--- /dev/null
+++ b/app/src/main/res/drawable/ic_play_arrow_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/ic_stop_24dp.xml b/app/src/main/res/drawable/ic_stop_24dp.xml
new file mode 100644
index 00000000..52ac20eb
--- /dev/null
+++ b/app/src/main/res/drawable/ic_stop_24dp.xml
@@ -0,0 +1,9 @@
+
+
+
diff --git a/app/src/main/res/drawable/toggle_button_color.xml b/app/src/main/res/drawable/toggle_button_color.xml
new file mode 100644
index 00000000..3385e972
--- /dev/null
+++ b/app/src/main/res/drawable/toggle_button_color.xml
@@ -0,0 +1,5 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml
new file mode 100644
index 00000000..a846cfe7
--- /dev/null
+++ b/app/src/main/res/layout/activity_main.xml
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/src/main/res/menu/settings.xml b/app/src/main/res/menu/settings.xml
new file mode 100644
index 00000000..257ad16e
--- /dev/null
+++ b/app/src/main/res/menu/settings.xml
@@ -0,0 +1,7 @@
+
+
\ No newline at end of file
diff --git a/app/src/main/res/mipmap-hdpi/ic_launcher.png b/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 00000000..1f7b30be
Binary files /dev/null and b/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-mdpi/ic_launcher.png b/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 00000000..fff7d94c
Binary files /dev/null and b/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 00000000..b86ea92f
Binary files /dev/null and b/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 00000000..f4f5e7cf
Binary files /dev/null and b/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 00000000..819989c9
Binary files /dev/null and b/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/app/src/main/res/values-ru/arrays.xml b/app/src/main/res/values-ru/arrays.xml
new file mode 100644
index 00000000..6af7c674
--- /dev/null
+++ b/app/src/main/res/values-ru/arrays.xml
@@ -0,0 +1,39 @@
+
+
+
+ - 100
+ - 90
+ - 80 - По умолчанию
+ - 70
+ - 60
+ - 50
+ - 40
+ - 30
+
+
+ - 100
+ - 90
+ - 80
+ - 70
+ - 60
+ - 50
+ - 40
+ - 30
+
+
+ - 5
+ - 4
+ - 3 - По умолчанию
+ - 2
+ - 1
+ - 0,5
+
+
+ - 5000
+ - 4000
+ - 3000
+ - 2000
+ - 1000
+ - 500
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values-ru/strings.xml b/app/src/main/res/values-ru/strings.xml
new file mode 100644
index 00000000..a7cdae78
--- /dev/null
+++ b/app/src/main/res/values-ru/strings.xml
@@ -0,0 +1,33 @@
+
+
+ НАЧАТЬ
+ ОСТАНОВИТЬ
+ Интерфейс
+ Настройки
+ Порт сервера
+ НАЖМИТЕ
+ НА УСТРОЙСТВЕ
+ WiFi не подключен
+ Адрес устойства:
+ ЗАКРЫТЬ
+ \u0020
+ Дополнительно
+ Установите порт для входяших подключений\nПо умолчанию: 8080
+ Качество JPEG
+ Установите качество JPEG
+ Останавливать при выключении
+ Останавливать поток если выключется экран
+ Отказано в доступе к изображению экрана
+ Время ответа клиента
+ Порт должен быть между %1$d и %2$d
+ Подключено клиентов: %1$d
+ Установите время ответа от клиента в секундах
+ Сворачивать при трансляции
+ Сворачивать приложение при начале трансляции
+ Нажмите начать для старта трансляции
+ Готов к трансляции
+ Начать трансляцию
+ Остановить трансляцию
+ Транслирую…
+ \nЗначение:\u0020
+
\ 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 00000000..8e283545
--- /dev/null
+++ b/app/src/main/res/values/arrays.xml
@@ -0,0 +1,39 @@
+
+
+
+ - 100
+ - 90
+ - 80 - Default
+ - 70
+ - 60
+ - 50
+ - 40
+ - 30
+
+
+ - 100
+ - 90
+ - 80
+ - 70
+ - 60
+ - 50
+ - 40
+ - 30
+
+
+ - 5s
+ - 4s
+ - 3s - Default
+ - 2s
+ - 1s
+ - 0.5s
+
+
+ - 5000
+ - 4000
+ - 3000
+ - 2000
+ - 1000
+ - 500
+
+
\ No newline at end of file
diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml
new file mode 100644
index 00000000..201e495f
--- /dev/null
+++ b/app/src/main/res/values/colors.xml
@@ -0,0 +1,7 @@
+
+
+ #607d8b
+ #455a64
+ #993200
+ #455a64
+
diff --git a/app/src/main/res/values/dimens.xml b/app/src/main/res/values/dimens.xml
new file mode 100644
index 00000000..16ccb714
--- /dev/null
+++ b/app/src/main/res/values/dimens.xml
@@ -0,0 +1,4 @@
+
+ 16dp
+ 8dp
+
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
new file mode 100644
index 00000000..fbba5f05
--- /dev/null
+++ b/app/src/main/res/values/strings.xml
@@ -0,0 +1,41 @@
+
+ Screen Stream
+
+
+ Device address:
+ Start stream
+ Stop stream
+ Connected clients: %1$d
+ Settings
+ No WiFi connected
+ Screen Cast permission denied
+
+
+ PRESS
+ ON DEVICE
+ Ready to stream
+ Press start to begin stream
+ START
+ CLOSE
+ Stream…
+ Go to:\u0020
+ STOP
+
+
+ Port number must be between %1$d and %2$d
+ \nValue:\u0020
+
+
+ Interface
+ Minimize on stream
+ Minimize app on stream start
+ Stop on sleep
+ Stop stream if screen turns off
+ Advanced
+ Server port
+ Set port for incoming connections\nDefault: 8080
+ JPEG quality
+ Set JPEG quality
+ Client connection timeout
+ Set time for client response in seconds
+
diff --git a/app/src/main/res/values/styles.xml b/app/src/main/res/values/styles.xml
new file mode 100644
index 00000000..4bfb0be4
--- /dev/null
+++ b/app/src/main/res/values/styles.xml
@@ -0,0 +1,9 @@
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml
new file mode 100644
index 00000000..9e70f072
--- /dev/null
+++ b/app/src/main/res/xml/preferences.xml
@@ -0,0 +1,47 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/build.gradle b/build.gradle
new file mode 100644
index 00000000..1ce391ee
--- /dev/null
+++ b/build.gradle
@@ -0,0 +1,24 @@
+// Top-level build file where you can add configuration options common to all sub-projects/modules.
+
+buildscript {
+ repositories {
+ jcenter()
+ }
+ dependencies {
+ classpath 'com.android.tools.build:gradle:2.2.0-alpha5'
+ classpath 'com.google.gms:google-services:3.0.0'
+
+ // NOTE: Do not place your application dependencies here; they belong
+ // in the individual module build.gradle files
+ }
+}
+
+allprojects {
+ repositories {
+ jcenter()
+ }
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/gradle.properties b/gradle.properties
new file mode 100644
index 00000000..1d3591c8
--- /dev/null
+++ b/gradle.properties
@@ -0,0 +1,18 @@
+# Project-wide Gradle settings.
+
+# IDE (e.g. Android Studio) users:
+# Gradle settings configured through the IDE *will override*
+# any settings specified in this file.
+
+# For more details on how to configure your build environment visit
+# http://www.gradle.org/docs/current/userguide/build_environment.html
+
+# Specifies the JVM arguments used for the daemon process.
+# The setting is particularly useful for tweaking memory settings.
+# Default value: -Xmx10248m -XX:MaxPermSize=256m
+# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8
+
+# When configured, Gradle will run in incubating parallel mode.
+# 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
\ No newline at end of file
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 00000000..13372aef
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 00000000..122a0dca
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Mon Dec 28 10:00:20 PST 2015
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-2.10-all.zip
diff --git a/gradlew b/gradlew
new file mode 100644
index 00000000..9d82f789
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,160 @@
+#!/usr/bin/env bash
+
+##############################################################################
+##
+## Gradle start up script for UN*X
+##
+##############################################################################
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS=""
+
+APP_NAME="Gradle"
+APP_BASE_NAME=`basename "$0"`
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD="maximum"
+
+warn ( ) {
+ echo "$*"
+}
+
+die ( ) {
+ echo
+ echo "$*"
+ echo
+ exit 1
+}
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+case "`uname`" in
+ CYGWIN* )
+ cygwin=true
+ ;;
+ Darwin* )
+ darwin=true
+ ;;
+ MINGW* )
+ msys=true
+ ;;
+esac
+
+# Attempt to set APP_HOME
+# Resolve links: $0 may be a link
+PRG="$0"
+# Need this for relative symlinks.
+while [ -h "$PRG" ] ; do
+ ls=`ls -ld "$PRG"`
+ link=`expr "$ls" : '.*-> \(.*\)$'`
+ if expr "$link" : '/.*' > /dev/null; then
+ PRG="$link"
+ else
+ PRG=`dirname "$PRG"`"/$link"
+ fi
+done
+SAVED="`pwd`"
+cd "`dirname \"$PRG\"`/" >/dev/null
+APP_HOME="`pwd -P`"
+cd "$SAVED" >/dev/null
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD="$JAVA_HOME/jre/sh/java"
+ else
+ JAVACMD="$JAVA_HOME/bin/java"
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD="java"
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if [ "$cygwin" = "false" -a "$darwin" = "false" ] ; then
+ MAX_FD_LIMIT=`ulimit -H -n`
+ if [ $? -eq 0 ] ; then
+ if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
+ MAX_FD="$MAX_FD_LIMIT"
+ fi
+ ulimit -n $MAX_FD
+ if [ $? -ne 0 ] ; then
+ warn "Could not set maximum file descriptor limit: $MAX_FD"
+ fi
+ else
+ warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
+ fi
+fi
+
+# For Darwin, add options to specify how the application appears in the dock
+if $darwin; then
+ GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
+fi
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin ; then
+ APP_HOME=`cygpath --path --mixed "$APP_HOME"`
+ CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
+ JAVACMD=`cygpath --unix "$JAVACMD"`
+
+ # We build the pattern for arguments to be converted via cygpath
+ ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
+ SEP=""
+ for dir in $ROOTDIRSRAW ; do
+ ROOTDIRS="$ROOTDIRS$SEP$dir"
+ SEP="|"
+ done
+ OURCYGPATTERN="(^($ROOTDIRS))"
+ # Add a user-defined pattern to the cygpath arguments
+ if [ "$GRADLE_CYGPATTERN" != "" ] ; then
+ OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
+ fi
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ i=0
+ for arg in "$@" ; do
+ CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
+ CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
+
+ if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
+ eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
+ else
+ eval `echo args$i`="\"$arg\""
+ fi
+ i=$((i+1))
+ done
+ case $i in
+ (0) set -- ;;
+ (1) set -- "$args0" ;;
+ (2) set -- "$args0" "$args1" ;;
+ (3) set -- "$args0" "$args1" "$args2" ;;
+ (4) set -- "$args0" "$args1" "$args2" "$args3" ;;
+ (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
+ (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
+ (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
+ (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
+ (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
+ esac
+fi
+
+# Split up the JVM_OPTS And GRADLE_OPTS values into an array, following the shell quoting and substitution rules
+function splitJvmOpts() {
+ JVM_OPTS=("$@")
+}
+eval splitJvmOpts $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS
+JVM_OPTS[${#JVM_OPTS[*]}]="-Dorg.gradle.appname=$APP_BASE_NAME"
+
+exec "$JAVACMD" "${JVM_OPTS[@]}" -classpath "$CLASSPATH" org.gradle.wrapper.GradleWrapperMain "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 00000000..8a0b282a
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,90 @@
+@if "%DEBUG%" == "" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS=
+
+set DIRNAME=%~dp0
+if "%DIRNAME%" == "" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if "%ERRORLEVEL%" == "0" goto init
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto init
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:init
+@rem Get command-line arguments, handling Windowz variants
+
+if not "%OS%" == "Windows_NT" goto win9xME_args
+if "%@eval[2+2]" == "4" goto 4NT_args
+
+:win9xME_args
+@rem Slurp the command line arguments.
+set CMD_LINE_ARGS=
+set _SKIP=2
+
+:win9xME_args_slurp
+if "x%~1" == "x" goto execute
+
+set CMD_LINE_ARGS=%*
+goto execute
+
+:4NT_args
+@rem Get arguments from the 4NT Shell from JP Software
+set CMD_LINE_ARGS=%$
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS%
+
+:end
+@rem End local scope for the variables with windows NT shell
+if "%ERRORLEVEL%"=="0" goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
+exit /b 1
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/settings.gradle b/settings.gradle
new file mode 100644
index 00000000..e7b4def4
--- /dev/null
+++ b/settings.gradle
@@ -0,0 +1 @@
+include ':app'