diff --git a/.gitignore b/.gitignore index ccf2efe..80a6566 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,14 @@ *.apk *.ap_ +# Exclusion to allow committing the game apk +!∞ Loop_v3_0.apk + +# AndroidStudio files +*.iml +.idea +/captures + # Files for the Dalvik VM *.dex @@ -25,3 +33,6 @@ proguard/ # Log Files *.log + +# Dreaded DS_Store +.DS_Store diff --git a/app/build.gradle b/app/build.gradle new file mode 100644 index 0000000..7f55b4b --- /dev/null +++ b/app/build.gradle @@ -0,0 +1,28 @@ +apply plugin: 'com.android.application' + +android { + compileSdkVersion 23 + buildToolsVersion "19.1.0" + + defaultConfig { + applicationId "efokschaner.infinityloopsolver" + minSdkVersion 23 + targetSdkVersion 23 + versionCode 1 + versionName "1.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } +} + +dependencies { + compile fileTree(dir: 'libs', include: ['*.jar']) + testCompile 'junit:junit:4.12' + compile 'com.android.support:appcompat-v7:23.1.1' + compile 'com.android.support:support-v4:23.1.1' + compile 'com.android.support:design:23.1.1' +} diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro new file mode 100644 index 0000000..91f6bd9 --- /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 /Users/efokschaner/Library/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/androidTest/java/efokschaner/infinityloopsolver/ApplicationTest.java b/app/src/androidTest/java/efokschaner/infinityloopsolver/ApplicationTest.java new file mode 100644 index 0000000..a24d864 --- /dev/null +++ b/app/src/androidTest/java/efokschaner/infinityloopsolver/ApplicationTest.java @@ -0,0 +1,13 @@ +package efokschaner.infinityloopsolver; + +import android.app.Application; +import android.test.ApplicationTestCase; + +/** + * Testing Fundamentals + */ +public class ApplicationTest extends ApplicationTestCase { + public ApplicationTest() { + super(Application.class); + } +} diff --git a/app/src/androidTest/java/efokschaner/infinityloopsolver/UiAutomationTest.java b/app/src/androidTest/java/efokschaner/infinityloopsolver/UiAutomationTest.java new file mode 100644 index 0000000..f7ec5e5 --- /dev/null +++ b/app/src/androidTest/java/efokschaner/infinityloopsolver/UiAutomationTest.java @@ -0,0 +1,136 @@ +package efokschaner.infinityloopsolver; + + +import android.accessibilityservice.AccessibilityServiceInfo; +import android.app.Instrumentation; +import android.app.UiAutomation; +import android.content.Context; +import android.content.Intent; +import android.content.pm.PackageManager; +import android.graphics.Bitmap; +import android.test.InstrumentationTestCase; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.List; +import java.util.concurrent.TimeoutException; + + +// Try using Context.startInstrumentation to move this functionality back into app +// Need to derive from Instrumentation class itself and implement onStart (see InstrumentationTestRunner) +public class UiAutomationTest extends InstrumentationTestCase { + private static final String TAG = UiAutomationTest.class.getSimpleName(); + + private static final Runnable NOOP = new Runnable() { public void run() {} }; + + private void sendBitmap(Bitmap bitmap) { + try { + String timestamp = new SimpleDateFormat("HH_mm_ss").format(new Date()); + URL url = new URL("http://10.0.2.2:8888/" + timestamp + ".png"); + try { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + try { + conn.setDoOutput(true); + conn.setChunkedStreamingMode(0); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/octet-stream"); + try (OutputStream ostream = conn.getOutputStream()) { + bitmap.compress(Bitmap.CompressFormat.PNG, 100, ostream); + } catch (IOException e) { + e.printStackTrace(); + } + final int responseCode = conn.getResponseCode(); + if (!(responseCode >= 200 && responseCode < 300)) { + throw new AssertionError(String.format("Http response was: %d", responseCode)); + } + conn.getResponseMessage(); + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + while (in.readLine() != null){ + // ignore contents + } + in.close(); + } catch (ProtocolException e) { + e.printStackTrace(); + } finally { + conn.disconnect(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + + private static AccessibilityNodeInfo findInfinityLoopView(AccessibilityNodeInfo node) { + final String viewIdResourceName = node.getViewIdResourceName(); + if(viewIdResourceName != null && viewIdResourceName.equals("com.balysv.loop:id/game_scene_view_light")) { + return node; + } + final int numChildren = node.getChildCount(); + for(int i = 0; i < numChildren; ++i) { + AccessibilityNodeInfo childNode; + if((childNode = findInfinityLoopView(node.getChild(i))) != null) { + return childNode; + } + } + return null; + } + + private static boolean isInfinityLoopReady(List windows) { + // Determine if InfinityLoop (and only InfinityLoop) is on screen + return (windows.size() == 1 && + (findInfinityLoopView(windows.get(0).getRoot())) != null); + } + + private static boolean isInfinityLoopReady(UiAutomation uiAutomation, AccessibilityEvent event) { + if(event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + final List windows = uiAutomation.getWindows(); + return isInfinityLoopReady(windows); + } else { + return false; + } + } + + public void test() throws TimeoutException { + Log.d(TAG, "test()"); + final Instrumentation instrumentation = getInstrumentation(); + final UiAutomation uiAutomation = instrumentation.getUiAutomation(); + final AccessibilityServiceInfo serviceInfo = uiAutomation.getServiceInfo(); + serviceInfo.flags |= AccessibilityServiceInfo.FLAG_RETRIEVE_INTERACTIVE_WINDOWS; + uiAutomation.setServiceInfo(serviceInfo); + Log.d(TAG, uiAutomation.getServiceInfo().toString()); + final Context context = instrumentation.getContext(); + final PackageManager packageManager = context.getPackageManager(); + final Intent launchIntent = packageManager.getLaunchIntentForPackage("com.balysv.loop"); + if(launchIntent != null) { + context.startActivity(launchIntent); + } + if(!isInfinityLoopReady(uiAutomation.getWindows())) { + uiAutomation.executeAndWaitForEvent(NOOP, new UiAutomation.AccessibilityEventFilter() { + @Override + public boolean accept(AccessibilityEvent event) { + return isInfinityLoopReady(uiAutomation, event); + } + }, 10000); + } + Bitmap b = uiAutomation.takeScreenshot(); + try{ + sendBitmap(b); + } finally { + b.recycle(); + } + } +} diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..5752426 --- /dev/null +++ b/app/src/main/AndroidManifest.xml @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/app/src/main/java/efokschaner/infinityloopsolver/AccessibilityService.java b/app/src/main/java/efokschaner/infinityloopsolver/AccessibilityService.java new file mode 100644 index 0000000..2c211fc --- /dev/null +++ b/app/src/main/java/efokschaner/infinityloopsolver/AccessibilityService.java @@ -0,0 +1,82 @@ +package efokschaner.infinityloopsolver; + +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.os.IBinder; +import android.util.Log; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowInfo; + +import java.util.List; + + +public class AccessibilityService extends android.accessibilityservice.AccessibilityService { + private static final String TAG = AccessibilityService.class.getSimpleName(); + + private AccessibilityNodeInfo mGameView; + private SolverService mSolverService; + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mSolverService = ((SolverService.SolverServiceBinder)service).getService(); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mSolverService = null; + } + }; + + @Override + protected void onServiceConnected() { + Log.d(TAG, "onServiceConnected"); + super.onServiceConnected(); + bindService(new Intent(this, SolverService.class), mConnection, Context.BIND_AUTO_CREATE); + } + + private AccessibilityNodeInfo findInfinityLoopView(AccessibilityNodeInfo node) { + final String viewIdResourceName = node.getViewIdResourceName(); + if(viewIdResourceName != null && viewIdResourceName.equals("com.balysv.loop:id/game_scene_view_light")) { + return node; + } + final int numChildren = node.getChildCount(); + for(int i = 0; i < numChildren; ++i) { + AccessibilityNodeInfo childNode; + if((childNode = findInfinityLoopView(node.getChild(i))) != null) { + return childNode; + } + } + return null; + } + + @Override + public void onAccessibilityEvent(AccessibilityEvent event) { + if(mSolverService == null) { + // Ignore all events until we're connected to the solver service + return; + } + if(event.getEventType() == AccessibilityEvent.TYPE_WINDOWS_CHANGED) { + final List windows = getWindows(); + // Determine if InfinityLoop (and only InfinityLoop) is on screen + if(windows.size() == 1 && (mGameView = findInfinityLoopView(windows.get(0).getRoot())) != null) { + mSolverService.SetInfinityLoopIsFocused(true); + } else { + mSolverService.SetInfinityLoopIsFocused(false); + } + } + } + + @Override + public void onInterrupt() { + } + + @Override + public boolean onUnbind(Intent i) { + Log.d(TAG, "onUnbind"); + unbindService(mConnection); + return false; + } +} diff --git a/app/src/main/java/efokschaner/infinityloopsolver/AppCompatPreferenceActivity.java b/app/src/main/java/efokschaner/infinityloopsolver/AppCompatPreferenceActivity.java new file mode 100644 index 0000000..5f5f8e1 --- /dev/null +++ b/app/src/main/java/efokschaner/infinityloopsolver/AppCompatPreferenceActivity.java @@ -0,0 +1,109 @@ +package efokschaner.infinityloopsolver; + +import android.content.res.Configuration; +import android.os.Bundle; +import android.preference.PreferenceActivity; +import android.support.annotation.LayoutRes; +import android.support.annotation.Nullable; +import android.support.v7.app.ActionBar; +import android.support.v7.app.AppCompatDelegate; +import android.support.v7.widget.Toolbar; +import android.view.MenuInflater; +import android.view.View; +import android.view.ViewGroup; + +/** + * A {@link android.preference.PreferenceActivity} which implements and proxies the necessary calls + * to be used with AppCompat. + */ +public abstract class AppCompatPreferenceActivity extends PreferenceActivity { + + private AppCompatDelegate mDelegate; + + @Override + protected void onCreate(Bundle savedInstanceState) { + getDelegate().installViewFactory(); + getDelegate().onCreate(savedInstanceState); + super.onCreate(savedInstanceState); + } + + @Override + protected void onPostCreate(Bundle savedInstanceState) { + super.onPostCreate(savedInstanceState); + getDelegate().onPostCreate(savedInstanceState); + } + + public ActionBar getSupportActionBar() { + return getDelegate().getSupportActionBar(); + } + + public void setSupportActionBar(@Nullable Toolbar toolbar) { + getDelegate().setSupportActionBar(toolbar); + } + + @Override + public MenuInflater getMenuInflater() { + return getDelegate().getMenuInflater(); + } + + @Override + public void setContentView(@LayoutRes int layoutResID) { + getDelegate().setContentView(layoutResID); + } + + @Override + public void setContentView(View view) { + getDelegate().setContentView(view); + } + + @Override + public void setContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().setContentView(view, params); + } + + @Override + public void addContentView(View view, ViewGroup.LayoutParams params) { + getDelegate().addContentView(view, params); + } + + @Override + protected void onPostResume() { + super.onPostResume(); + getDelegate().onPostResume(); + } + + @Override + protected void onTitleChanged(CharSequence title, int color) { + super.onTitleChanged(title, color); + getDelegate().setTitle(title); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + super.onConfigurationChanged(newConfig); + getDelegate().onConfigurationChanged(newConfig); + } + + @Override + protected void onStop() { + super.onStop(); + getDelegate().onStop(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + getDelegate().onDestroy(); + } + + public void invalidateOptionsMenu() { + getDelegate().invalidateOptionsMenu(); + } + + private AppCompatDelegate getDelegate() { + if (mDelegate == null) { + mDelegate = AppCompatDelegate.create(this, null); + } + return mDelegate; + } +} diff --git a/app/src/main/java/efokschaner/infinityloopsolver/MainActivity.java b/app/src/main/java/efokschaner/infinityloopsolver/MainActivity.java new file mode 100644 index 0000000..cf15640 --- /dev/null +++ b/app/src/main/java/efokschaner/infinityloopsolver/MainActivity.java @@ -0,0 +1,26 @@ +package efokschaner.infinityloopsolver; + +import android.content.Intent; +import android.provider.Settings; +import android.support.v7.app.AppCompatActivity; +import android.os.Bundle; +import android.view.View; +import android.widget.Button; + +public class MainActivity extends AppCompatActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_main); + final Button button = (Button) findViewById(R.id.button); + button.setOnClickListener(new View.OnClickListener() { + @Override + public void onClick(View v) { + Intent settingsActivityIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS); + settingsActivityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); + startActivity(settingsActivityIntent); + } + }); + } +} diff --git a/app/src/main/java/efokschaner/infinityloopsolver/MediaProjectionRequest.java b/app/src/main/java/efokschaner/infinityloopsolver/MediaProjectionRequest.java new file mode 100644 index 0000000..097b29c --- /dev/null +++ b/app/src/main/java/efokschaner/infinityloopsolver/MediaProjectionRequest.java @@ -0,0 +1,62 @@ +package efokschaner.infinityloopsolver; + +import android.app.Activity; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.content.ServiceConnection; +import android.media.projection.MediaProjection; +import android.media.projection.MediaProjectionManager; +import android.os.Bundle; +import android.os.IBinder; +import android.util.DisplayMetrics; +import android.util.Log; + +public class MediaProjectionRequest extends Activity { + private static final String TAG = MediaProjectionRequest.class.getSimpleName(); + + private MediaProjectionManager mMediaProjectionManager; + private SolverService mSolverService; + + private ServiceConnection mConnection = new ServiceConnection() { + @Override + public void onServiceConnected(ComponentName name, IBinder service) { + mSolverService = ((SolverService.SolverServiceBinder)service).getService(); + startActivityForResult(mMediaProjectionManager.createScreenCaptureIntent(), 0); + } + + @Override + public void onServiceDisconnected(ComponentName name) { + mSolverService = null; + } + }; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + mMediaProjectionManager = (MediaProjectionManager) getSystemService(MEDIA_PROJECTION_SERVICE); + bindService(new Intent(this, SolverService.class), mConnection, Context.BIND_AUTO_CREATE); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + MediaProjection mp = mMediaProjectionManager.getMediaProjection(resultCode, data); + if(mp != null) { + Log.d(TAG, "Accepted"); + DisplayMetrics metrics = new DisplayMetrics(); + getWindowManager().getDefaultDisplay().getMetrics(metrics); + mSolverService.SetMediaHandles(new SolverService.MediaHandles(metrics, mp)); + } else { + Log.d(TAG, "Declined"); + } + + super.onActivityResult(requestCode, resultCode, data); + finish(); + } + + @Override + protected void onDestroy() { + unbindService(mConnection); + super.onDestroy(); + } +} diff --git a/app/src/main/java/efokschaner/infinityloopsolver/SettingsActivity.java b/app/src/main/java/efokschaner/infinityloopsolver/SettingsActivity.java new file mode 100644 index 0000000..14ce4d2 --- /dev/null +++ b/app/src/main/java/efokschaner/infinityloopsolver/SettingsActivity.java @@ -0,0 +1,154 @@ +package efokschaner.infinityloopsolver; + + +import android.annotation.TargetApi; +import android.content.Context; +import android.content.Intent; +import android.content.SharedPreferences; +import android.content.res.Configuration; +import android.os.Build; +import android.os.Bundle; +import android.preference.Preference; +import android.preference.PreferenceActivity; +import android.preference.SwitchPreference; +import android.support.v7.app.ActionBar; +import android.preference.PreferenceFragment; +import android.preference.PreferenceManager; +import android.view.MenuItem; + +import java.util.List; + +/** + * A {@link PreferenceActivity} that presents a set of application settings. On + * handset devices, settings are presented as a single list. On tablets, + * settings are split by category, with category headers shown to the left of + * the list of settings. + *

+ * See + * Android Design: Settings for design guidelines and the Settings + * API Guide for more information on developing a Settings UI. + */ +public class SettingsActivity extends AppCompatPreferenceActivity { + /** + * A preference value change listener that updates the preference's summary + * to reflect its new value. + */ + private static Preference.OnPreferenceChangeListener sBindPreferenceSummaryToValueListener = new Preference.OnPreferenceChangeListener() { + @Override + public boolean onPreferenceChange(Preference preference, Object value) { + String stringValue = value.toString(); + // For all other preferences, set the summary to the value's + // simple string representation. + preference.setSummary(stringValue); + return true; + } + }; + + /** + * Helper method to determine if the device has an extra-large screen. For + * example, 10" tablets are extra-large. + */ + private static boolean isXLargeTablet(Context context) { + return (context.getResources().getConfiguration().screenLayout + & Configuration.SCREENLAYOUT_SIZE_MASK) >= Configuration.SCREENLAYOUT_SIZE_XLARGE; + } + + /** + * Binds a preference's summary to its value. More specifically, when the + * preference's value is changed, its summary (line of text below the + * preference title) is updated to reflect the value. The summary is also + * immediately updated upon calling this method. The exact display format is + * dependent on the type of preference. + * + * @see #sBindPreferenceSummaryToValueListener + */ + private static void bindPreferenceSummaryToValue(Preference preference) { + // Set the listener to watch for value changes. + preference.setOnPreferenceChangeListener(sBindPreferenceSummaryToValueListener); + + // Trigger the listener immediately with the preference's + // current value. + Object currentValue; + SharedPreferences defaultSharedPreferences = + PreferenceManager.getDefaultSharedPreferences(preference.getContext()); + if(preference instanceof SwitchPreference) { + currentValue = + defaultSharedPreferences.getBoolean(preference.getKey(), false); + } else { + currentValue = + defaultSharedPreferences.getString(preference.getKey(), ""); + } + sBindPreferenceSummaryToValueListener.onPreferenceChange(preference, currentValue); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setupActionBar(); + } + + /** + * Set up the {@link android.app.ActionBar}, if the API is available. + */ + private void setupActionBar() { + ActionBar actionBar = getSupportActionBar(); + if (actionBar != null) { + // Show the Up button in the action bar. + actionBar.setDisplayHomeAsUpEnabled(true); + } + } + + /** + * {@inheritDoc} + */ + @Override + public boolean onIsMultiPane() { + return isXLargeTablet(this); + } + + /** + * {@inheritDoc} + */ + @Override + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public void onBuildHeaders(List

target) { + loadHeadersFromResource(R.xml.pref_headers, target); + } + + /** + * This method stops fragment injection in malicious applications. + * Make sure to deny any unknown fragments here. + */ + protected boolean isValidFragment(String fragmentName) { + return PreferenceFragment.class.getName().equals(fragmentName) + || GeneralPreferenceFragment.class.getName().equals(fragmentName); + } + + /** + * This fragment shows general preferences only. It is used when the + * activity is showing a two-pane settings UI. + */ + @TargetApi(Build.VERSION_CODES.HONEYCOMB) + public static class GeneralPreferenceFragment extends PreferenceFragment { + @Override + public void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + addPreferencesFromResource(R.xml.pref_general); + setHasOptionsMenu(true); + + bindPreferenceSummaryToValue(findPreference("solver_enabled_switch")); + } + + @Override + public boolean onOptionsItemSelected(MenuItem item) { + int id = item.getItemId(); + if (id == android.R.id.home) { + startActivity(new Intent(getActivity(), SettingsActivity.class)); + return true; + } + return super.onOptionsItemSelected(item); + } + } + +} diff --git a/app/src/main/java/efokschaner/infinityloopsolver/SolverService.java b/app/src/main/java/efokschaner/infinityloopsolver/SolverService.java new file mode 100644 index 0000000..0452f75 --- /dev/null +++ b/app/src/main/java/efokschaner/infinityloopsolver/SolverService.java @@ -0,0 +1,221 @@ +package efokschaner.infinityloopsolver; + +import android.app.Service; +import android.app.UiAutomation; +import android.content.Intent; +import android.graphics.Bitmap; +import android.graphics.PixelFormat; +import android.hardware.display.DisplayManager; +import android.hardware.display.VirtualDisplay; +import android.media.Image; +import android.media.ImageReader; +import android.media.projection.MediaProjection; +import android.os.Binder; +import android.os.IBinder; +import android.util.DisplayMetrics; +import android.util.Log; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.ProtocolException; +import java.net.URL; +import java.nio.Buffer; +import java.text.SimpleDateFormat; +import java.util.Date; + +public class SolverService extends Service { + private static final String TAG = SolverService.class.getSimpleName(); + + public static class MediaHandles { + public DisplayMetrics metrics; + public MediaProjection mediaProjection; + + public MediaHandles(DisplayMetrics metrics, MediaProjection mediaProjection) { + this.metrics = metrics; + this.mediaProjection = mediaProjection; + } + } + + public void SetMediaHandles(MediaHandles mh) { + Log.d(TAG, "mMediaHandles set"); + mMediaHandles = mh; + StartOrStopSolver(); + } + + public void SetInfinityLoopIsFocused(boolean b) { + Log.d(TAG, String.format("mInfinityLoopIsFocused set to %s", b)); + mInfinityLoopIsFocused = b; + StartOrStopSolver(); + } + + private void SetServiceEnabled(boolean b) { + Log.d(TAG, String.format("mServiceEnabled set to %s", b)); + mServiceEnabled = b; + StartOrStopSolver(); + } + + public class SolverServiceBinder extends Binder { + SolverService getService() { + return SolverService.this; + } + } + + private final SolverServiceBinder mBinder = new SolverServiceBinder(); + + @Override + public IBinder onBind(Intent intent) { + SetServiceEnabled(true); + RequestMediaHandles(); + return mBinder; + } + + @Override + public boolean onUnbind(Intent intent) { + SetServiceEnabled(false); + return false; + } + + private Thread mSolverThread; + private boolean mServiceEnabled = false; + private boolean mInfinityLoopIsFocused = false; + private MediaHandles mMediaHandles; + + private void StartOrStopSolver() { + if(mServiceEnabled && mMediaHandles != null && mInfinityLoopIsFocused) { + StartSolver(); + } else { + ShutdownSolver(); + } + } + + private void RequestMediaHandles() { + Intent intent = new Intent(this, MediaProjectionRequest.class); + intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK | Intent.FLAG_ACTIVITY_REORDER_TO_FRONT); + startActivity(intent); + } + + private void StartSolver() { + if(mSolverThread == null) { + mSolverThread = new Thread(mRunnableSolver); + mSolverThread.start(); + } + } + + private void ShutdownSolver() { + Log.d(TAG, "Shutting down"); + if(mSolverThread != null) { + Thread t = mSolverThread; + mSolverThread = null; + t.interrupt(); + try { + t.join(); + } catch (InterruptedException e) { + // ignore + } + } + } + + public SolverService() { + } + + @Override + public void onCreate() { + super.onCreate(); + + } + + @Override + public void onDestroy() { + super.onDestroy(); + } + + private static void TryAcquireImage(ImageReader imageReader) { + try (Image image = imageReader.acquireLatestImage()) { + if(image != null) { + try { + String timestamp = new SimpleDateFormat("HH_mm_ss").format(new Date()); + URL url = new URL("http://10.0.2.2:8888/" + timestamp + ".png"); + try { + HttpURLConnection conn = (HttpURLConnection) url.openConnection(); + try { + conn.setDoOutput(true); + conn.setChunkedStreamingMode(0); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", "application/octet-stream"); + try (OutputStream ostream = conn.getOutputStream()) { + final Image.Plane[] planes = image.getPlanes(); + final Buffer buffer = planes[0].getBuffer(); + final int pixelStride = planes[0].getPixelStride(); + final int rowStride = planes[0].getRowStride(); + Bitmap bitmap = Bitmap.createBitmap(rowStride / pixelStride, image.getHeight(), Bitmap.Config.ARGB_8888); + try { + bitmap.copyPixelsFromBuffer(buffer); + bitmap.compress(Bitmap.CompressFormat.PNG, 100, ostream); + } finally { + bitmap.recycle(); + } + } catch (IOException e) { + e.printStackTrace(); + } + final int responseCode = conn.getResponseCode(); + if (!(responseCode >= 200 && responseCode < 300)) { + throw new AssertionError(String.format("Http response was: %d", responseCode)); + } + conn.getResponseMessage(); + BufferedReader in = new BufferedReader(new InputStreamReader(conn.getInputStream())); + while (in.readLine() != null){ + // ignore contents + } + in.close(); + } catch (ProtocolException e) { + e.printStackTrace(); + } finally { + conn.disconnect(); + } + } catch (IOException e) { + e.printStackTrace(); + } + } catch (MalformedURLException e) { + e.printStackTrace(); + } + } + } + } + + private Runnable mRunnableSolver = new Runnable() { + @Override + public void run() { + try (ImageReader imageReader = ImageReader.newInstance( + mMediaHandles.metrics.widthPixels, + mMediaHandles.metrics.heightPixels, + PixelFormat.RGBA_8888, + 2)) { + VirtualDisplay virtualDisplay = mMediaHandles.mediaProjection.createVirtualDisplay( + "ScreenCapture", + mMediaHandles.metrics.widthPixels, + mMediaHandles.metrics.heightPixels, + mMediaHandles.metrics.densityDpi, + DisplayManager.VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR, + imageReader.getSurface(), + null, + null); + try { + while(true) { + Log.d(TAG, "Running"); + TryAcquireImage(imageReader); + Thread.sleep(5000); + } + } finally { + virtualDisplay.release(); + } + } + catch (InterruptedException e) { + // ignore + } + } + }; +} diff --git a/app/src/main/res/drawable/ic_info_black_24dp.xml b/app/src/main/res/drawable/ic_info_black_24dp.xml new file mode 100644 index 0000000..34b8202 --- /dev/null +++ b/app/src/main/res/drawable/ic_info_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_notifications_black_24dp.xml b/app/src/main/res/drawable/ic_notifications_black_24dp.xml new file mode 100644 index 0000000..e3400cf --- /dev/null +++ b/app/src/main/res/drawable/ic_notifications_black_24dp.xml @@ -0,0 +1,9 @@ + + + diff --git a/app/src/main/res/drawable/ic_sync_black_24dp.xml b/app/src/main/res/drawable/ic_sync_black_24dp.xml new file mode 100644 index 0000000..3f0ac1c --- /dev/null +++ b/app/src/main/res/drawable/ic_sync_black_24dp.xml @@ -0,0 +1,9 @@ + + + \ 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 0000000..b14c316 --- /dev/null +++ b/app/src/main/res/layout/activity_main.xml @@ -0,0 +1,19 @@ + + + +