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'