Skip to content

Commit

Permalink
Exploration of AccessibilityService and instrumentation from tests
Browse files Browse the repository at this point in the history
  • Loading branch information
efokschaner committed Dec 30, 2015
1 parent fb9c9a0 commit 1796c0b
Show file tree
Hide file tree
Showing 39 changed files with 1,362 additions and 0 deletions.
11 changes: 11 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -25,3 +33,6 @@ proguard/

# Log Files
*.log

# Dreaded DS_Store
.DS_Store
28 changes: 28 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
17 changes: 17 additions & 0 deletions app/proguard-rules.pro
Original file line number Diff line number Diff line change
@@ -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 *;
#}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package efokschaner.infinityloopsolver;

import android.app.Application;
import android.test.ApplicationTestCase;

/**
* <a href="http://d.android.com/tools/testing/testing_android.html">Testing Fundamentals</a>
*/
public class ApplicationTest extends ApplicationTestCase<Application> {
public ApplicationTest() {
super(Application.class);
}
}
Original file line number Diff line number Diff line change
@@ -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<AccessibilityWindowInfo> 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<AccessibilityWindowInfo> 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();
}
}
}
52 changes: 52 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="efokschaner.infinityloopsolver">

<uses-permission android:name="android.permission.BIND_ACCESSIBILITY_SERVICE" />
<uses-permission android:name="android.permission.INTERNET" />

<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:supportsRtl="true"
android:theme="@style/AppTheme">

<activity
android:name=".MainActivity"
android:launchMode="singleTask">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>

<service
android:name=".AccessibilityService"
android:permission="android.permission.BIND_ACCESSIBILITY_SERVICE">
<intent-filter>
<action android:name="android.accessibilityservice.AccessibilityService" />
</intent-filter>

<meta-data
android:name="android.accessibilityservice"
android:resource="@xml/accessibility_service_config" />
</service>

<!--
<activity
android:name=".SettingsActivity"
android:label="@string/title_activity_settings" />
-->
<activity
android:name=".MediaProjectionRequest"
android:theme="@android:style/Theme.Translucent.NoTitleBar" />

<service
android:name=".SolverService"
android:enabled="true"
android:exported="false" />

</application>

</manifest>
Original file line number Diff line number Diff line change
@@ -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<AccessibilityWindowInfo> 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;
}
}
Loading

0 comments on commit 1796c0b

Please sign in to comment.