Browse Source

initial speech bubble

Fabio Arnold 9 years ago
parent
commit
b3b586df8e

BIN
assets/fonts/laCartoonerie.bin


BIN
assets/fonts/laCartoonerie.png


BIN
assets/textures/speech-bubble.png


+ 4 - 4
res/values/strings.xml

@@ -37,10 +37,10 @@
     <string name="helpPortbinder">Please follow the instructions in our website to install \'Portbinder\'.\n\nAlternatively, you can use the automated installer by pressing the \'Just Help Me!\' button.</string>
     <string name="confirm_msg">This automated installer fetches the appropriate Portbinder binary and installs in a location within the device.\n\nThis automated process will CHANGE some folder permissions to work. Proceed on your own risk.\n\nConfirm to proceed with automated installation of Portbinder?</string>
     <string name="help_me">Just Help Me!</string>
-	<string name="honeypot_not_monitoring"><![CDATA[<h3>Android says:</h3><p><i>&quot;I\'m currently not looking for attacks. Zzz...&quot;</i></p>]]></string>
-	<string name="honeypot_no_threat"><![CDATA[<h3>Android says:</h3><p><i>&quot;Looks safe!&quot;</i></p>]]></string>
-	<string name="honeypot_past_threat"><![CDATA[<h3>Android says:</h3><p><i>&quot;There have been attacks in this network! This doesn\'t look safe...&quot;</i></p>]]></string>
-	<string name="honeypot_live_threat"><![CDATA[<h3>Android says:</h3><p><i>&quot;Under attack!!!&quot;</i></p>]]></string>
+	<string name="honeypot_not_monitoring">Zzz...</string>
+	<string name="honeypot_no_threat">Looks safe!</string>
+	<string name="honeypot_past_threat">This doesn\'t look safe...</string>
+	<string name="honeypot_live_threat">Under attack!!!</string>
 
 	<string name="profile_needs_name">An profile needs a name. Please type in a name and press save again.</string>
     <string name="monitor_current_connection">Monitor current connection</string>

+ 0 - 1
src/de/tudarmstadt/informatik/hostage/ui/activity/MainActivity.java

@@ -229,7 +229,6 @@ public class MainActivity extends Activity {
 		}
 
 		// init threat indicator animation
-		ThreatIndicatorGLRenderer.assets = getAssets();
 		ThreatIndicatorGLRenderer.setThreatLevel(ThreatIndicatorGLRenderer.ThreatLevel.NOT_MONITORING);
 
 		// set background color

+ 133 - 1
src/de/tudarmstadt/informatik/hostage/ui/fragment/opengl/GLFont.java

@@ -1,8 +1,140 @@
 package de.tudarmstadt.informatik.hostage.ui.fragment.opengl;
 
+import android.content.res.AssetManager;
+import android.opengl.GLES10;
+import android.opengl.GLES20;
+import android.util.Log;
+
+import java.io.ByteArrayOutputStream;
+import java.io.DataInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
+import java.nio.IntBuffer;
+
+import de.tudarmstadt.informatik.hostage.ui.activity.MainActivity;
+
 /**
- * Created by fabio on 13.02.15.
+ * Created by Fabio Arnold on 13.02.15.
  */
 public class GLFont {
+	private class GlyphMetric {
+		float x0,y0,s0,t0; // top-left
+		float x1,y1,s1,t1; // bottom-right
+		float advance;
+	}
+
+	private int mTexture;
+	private int mVertexBuffer;
+	private GlyphMetric mMetrics[];
+
+	public GLFont(String texFilePath, String metricsFilePath) {
+		AssetManager assets = MainActivity.getInstance().getAssets();
+
+		mTexture = ThreatIndicatorGLRenderer.loadTexture(texFilePath);
+
+		InputStream is = null;
+		try {
+			is = assets.open(metricsFilePath);
+		} catch (IOException e) {
+			e.printStackTrace();
+			return;
+		}
+		//DataInputStream dis = new DataInputStream(is);
+		ByteArrayOutputStream out = new ByteArrayOutputStream();
+		try {
+			final int EOF = -1;
+			int len;
+			byte[] buffer = new byte[1 << 12];
+			while (EOF != (len = is.read(buffer)))
+				out.write(buffer, 0, len);
+		} catch (IOException e) {
+			e.printStackTrace();
+			return;
+		}
+
+		//data = ByteBuffer.wrap(out.toByteArray()); // doesn't work data needs to be direct
+		ByteBuffer data = ByteBuffer.allocateDirect(out.size());
+		data.order(ByteOrder.nativeOrder());
+		data.put(out.toByteArray());
+		data.position(0);
+
+		mMetrics = new GlyphMetric[96];
+		for (int i = 0; i < 96; i++) {
+			mMetrics[i] = new GlyphMetric();
+
+			mMetrics[i].x0 = data.getFloat();
+			mMetrics[i].y0 = data.getFloat();
+			mMetrics[i].s0 = data.getFloat();
+			mMetrics[i].t0 = data.getFloat();
+			mMetrics[i].x1 = data.getFloat();
+			mMetrics[i].y1 = data.getFloat();
+			mMetrics[i].s1 = data.getFloat();
+			mMetrics[i].t1 = data.getFloat();
+			mMetrics[i].advance = data.getFloat();
+		}
+
+		int buffers[] = new int[1];
+		GLES20.glGenBuffers(1, buffers, 0); // buffer names
+		mVertexBuffer = buffers[0];
+	}
+
+	public float getTextWidth(String text) {
+		float x = 0.0f;
+		for (int i = 0; i < text.length(); i++) {
+			x += mMetrics[(int)text.charAt(i)-32].advance;
+		}
+		return x;
+	}
+
+	public void drawText(int program, String text, float x, float y) {
+		int vertexCount = text.length() * 6;
+		int vertexSize = (2+2)*4; // size in bytes
+
+		FloatBuffer buffer = ByteBuffer.allocateDirect(vertexCount * vertexSize).order(ByteOrder.nativeOrder()).asFloatBuffer();
+		for (int i = 0; i < text.length(); i++) {
+			int glyph = ((int)text.charAt(i))-32;
+			GlyphMetric metric = mMetrics[glyph];
+
+			buffer.put(x+metric.x0); buffer.put(y+metric.y0);
+			buffer.put(metric.s0); buffer.put(metric.t1);
+			buffer.put(x+metric.x1); buffer.put(y+metric.y0);
+			buffer.put(metric.s1); buffer.put(metric.t1);
+			buffer.put(x+metric.x1); buffer.put(y+metric.y1);
+			buffer.put(metric.s1); buffer.put(metric.t0);
+
+			buffer.put(x+metric.x0); buffer.put(y+metric.y0);
+			buffer.put(metric.s0); buffer.put(metric.t1);
+			buffer.put(x+metric.x1); buffer.put(y+metric.y1);
+			buffer.put(metric.s1); buffer.put(metric.t0);
+			buffer.put(x+metric.x0); buffer.put(y+metric.y1);
+			buffer.put(metric.s0); buffer.put(metric.t0);
+
+			x += metric.advance;
+		}
+		buffer.position(0); // rewind
+
+		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, mTexture);
+
+		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mVertexBuffer);
+		GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexSize * vertexCount, buffer, GLES20.GL_STREAM_DRAW);
+
+		int positionIndex = GLES20.glGetAttribLocation(program, "position");
+		int texCoordIndex = GLES20.glGetAttribLocation(program, "texCoord");
+
+		GLES20.glEnableVertexAttribArray(positionIndex);
+		GLES20.glEnableVertexAttribArray(texCoordIndex);
+
+		GLES20.glVertexAttribPointer(positionIndex, 2, GLES20.GL_FLOAT, false, vertexSize, 0);
+		GLES20.glVertexAttribPointer(texCoordIndex, 2, GLES20.GL_FLOAT, false, vertexSize, 8);
+
+		GLES20.glDrawArrays(GLES20.GL_TRIANGLES, 0, vertexCount);
+
+		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
 
+		GLES20.glDisableVertexAttribArray(positionIndex);
+		GLES20.glDisableVertexAttribArray(texCoordIndex);
+	}
 }

+ 183 - 64
src/de/tudarmstadt/informatik/hostage/ui/fragment/opengl/ThreatIndicatorGLRenderer.java

@@ -2,11 +2,15 @@ package de.tudarmstadt.informatik.hostage.ui.fragment.opengl;
 
 import java.io.IOException;
 import java.io.InputStream;
+import java.nio.ByteBuffer;
+import java.nio.ByteOrder;
+import java.nio.FloatBuffer;
 import java.util.Scanner;
 
 import javax.microedition.khronos.egl.EGLConfig;
 import javax.microedition.khronos.opengles.GL10;
 
+import android.content.Context;
 import android.content.res.AssetManager;
 import android.graphics.Bitmap;
 import android.graphics.BitmapFactory;
@@ -18,6 +22,9 @@ import android.opengl.GLUtils;
 import android.opengl.Matrix;
 import android.util.Log;
 
+import de.tudarmstadt.informatik.hostage.R;
+import de.tudarmstadt.informatik.hostage.ui.activity.MainActivity;
+
 /**
  * @author Fabio Arnold
  * 
@@ -52,11 +59,10 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		mBackgroundColor[2] = (float)Color.blue(color) / 255.0f;
 	}
 	private static float[] mBackgroundColor = new float[3];
-	
-	public static AssetManager assets = null; // needs to be set by the Activity using this class
 
 	// OpenGL data
-	private int mProgram;
+	private int mAnimatedProgram;
+	private int mTexturedProgram;
 	private float [] mModelview;
 	private float [] mProjection;
 	private float [] mMVP;
@@ -65,6 +71,10 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 	private AnimatedMesh beeMesh = null;
 	private int androidTexture;
 	private int beeTexture;
+	private int speechBubbleTexture;
+	private int mQuadVertexBuffer;
+
+	private GLFont font = null;
 	
 	// threat state
 	private static ThreatLevel mNextThreatLevel = ThreatLevel.NO_THREAT;
@@ -88,7 +98,8 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		GLES20.glEnable(GLES20.GL_DEPTH_TEST);
 		GLES20.glEnable(GLES20.GL_CULL_FACE);
 		GLES20.glEnable(GLES20.GL_TEXTURE_2D);
-		
+
+		AssetManager assets = MainActivity.getInstance().getAssets();
 		try {
 			InputStream is = assets.open("meshes/android.amh");
 			androidMesh = new AnimatedMesh(is);
@@ -112,6 +123,12 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 			Log.e("gl", "Couldn't open bee mesh");
 		}
 		beeTexture = loadTexture("textures/bee-tex.png");
+		speechBubbleTexture = loadTexture("textures/speech-bubble.png");
+		int buffers[] = new int[1];
+		GLES20.glGenBuffers(1, buffers, 0); // buffer names
+		mQuadVertexBuffer = buffers[0];
+
+		font = new GLFont("fonts/laCartoonerie.png", "fonts/laCartoonerie.bin");
 		
 		mModelview = new float[16];
 		Matrix.setIdentityM(mModelview, 0);
@@ -130,38 +147,49 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		} catch (IOException e) {
 			e.printStackTrace();
 		}
-		mProgram = loadProgram(vertexSource, fragmentSource);
+		mAnimatedProgram = loadProgram(vertexSource, fragmentSource);
+
+		mTexturedProgram = loadProgram(
+				  "uniform vec2 resolution;" // vertex
+				+ "attribute vec2 position;"
+				+ "attribute vec2 texCoord;"
+				+ "varying vec2 vertexTexCoord;"
+				+ "void main() {"
+				+ "	vertexTexCoord = texCoord;"
+				+ "	gl_Position = vec4(2.0 * (position / resolution) - 1.0, 0.0, 1.0);"
+				+ "}",
+				  "uniform sampler2D colormap;" // fragment
+				+ "uniform vec3 color;"
+				+ "varying vec2 vertexTexCoord;"
+				+ "void main() {"
+				+ " vec4 texel = texture2D(colormap, vertexTexCoord);"
+				+ "	gl_FragColor = vec4(color * texel.rgb, texel.a);"
+				+ "}");
 	}
-	
-	/**
-	 * Tries to render at 30 Hz (see bottom part)
-	 */
-	public void onDrawFrame(GL10 arg0) {
-		long timeMillis = System.currentTimeMillis() - mStartTimeMillis;
-		double animTime = 0.001 * (double)timeMillis; // in seconds
-		
+
+	private void updateAndroidAndBee() {
 		// threat level state machine
 		if (mTargetThreatLevel != mCurrentThreatLevel) {
 			boolean blocked = false; // block until current action is completed
 			if (mThreatLevelTransition == 0.0f) {
 				if (androidMesh.isActionDone()) {
 					switch (mTargetThreatLevel) {
-					case NOT_MONITORING:
-						androidMesh.startAction("sleep", false, false);
-						break;
-					case NO_THREAT:
-						androidMesh.startAction("happy", true, false);
-						break;
-					case PAST_THREAT:
-						androidMesh.startAction("fear", true, false);
-						break;
-					case LIVE_THREAT:
-						androidMesh.startAction("panic", true, false);
-						break;
+						case NOT_MONITORING:
+							androidMesh.startAction("sleep", false, false);
+							break;
+						case NO_THREAT:
+							androidMesh.startAction("happy", true, false);
+							break;
+						case PAST_THREAT:
+							androidMesh.startAction("fear", true, false);
+							break;
+						case LIVE_THREAT:
+							androidMesh.startAction("panic", true, false);
+							break;
 					}
 				} else blocked = true;
 			}
-			
+
 			if (!blocked) {
 				mThreatLevelTransition += 0.016f;
 				if (mThreatLevelTransition >= 1.0f) {
@@ -173,60 +201,59 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 			if (mNextThreatLevel != mTargetThreatLevel) {
 				mTargetThreatLevel = mNextThreatLevel;
 				mThreatLevelTransition = 0.0f;
-				
+
 				// HACK!!! reverses the sleep animation to create smooth transition into other states
 				if (mCurrentThreatLevel == ThreatLevel.NOT_MONITORING)
 					androidMesh.startAction("sleep", false, true);
 			}
 		}
-		
+
 		androidMesh.tick(); // animate android
-		
-		// OpenGL drawing
-		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
-		
-		GLES20.glUseProgram(mProgram);
-		int colorUniformLoc = GLES20.glGetUniformLocation(mProgram, "color");
-		int textureUniformLoc = GLES20.glGetUniformLocation(mProgram, "texture");
-		int mvpUniformLoc = GLES20.glGetUniformLocation(mProgram, "mvp");
+	}
+
+	private void drawAndroidAndBee(double animTime) {
+		GLES20.glUseProgram(mAnimatedProgram);
+		int colorUniformLoc = GLES20.glGetUniformLocation(mAnimatedProgram, "color");
+		int textureUniformLoc = GLES20.glGetUniformLocation(mAnimatedProgram, "texture");
+		int mvpUniformLoc = GLES20.glGetUniformLocation(mAnimatedProgram, "mvp");
 
 		//  animate color
 		final float[] whiteColor = {1.0f, 1.0f, 1.0f, 1.0f};
 		final float[] greyColor = {0.5f, 0.5f, 0.5f, 1.0f};
 		final float[] redColor = {2.0f, 0.4f, 0.2f, 1.0f};
 		final float[] yellowColor = {1.1f * 255.0f / 166.0f, 1.2f * 255.0f / 200.0f, 0.0f, 1.0f};
-		
+
 		float[] currentColor = whiteColor;
 		float blink = 0.5f + 0.5f * (float)Math.sin(12.0 * animTime);
 		switch (mCurrentThreatLevel) {
-		case NOT_MONITORING:
-			currentColor = greyColor;
-			break;
-		case PAST_THREAT:
-			currentColor = mixColor(blink, whiteColor, yellowColor);
-			break;
-		case LIVE_THREAT:
-			currentColor = mixColor(blink, whiteColor, redColor);
-			break;
-		}
-		if (mTargetThreatLevel != mCurrentThreatLevel) {
-			float[] targetColor = whiteColor;
-			switch (mTargetThreatLevel) {
 			case NOT_MONITORING:
-				targetColor = greyColor;
+				currentColor = greyColor;
 				break;
 			case PAST_THREAT:
-				targetColor = mixColor(blink, whiteColor, yellowColor);
+				currentColor = mixColor(blink, whiteColor, yellowColor);
 				break;
 			case LIVE_THREAT:
-				targetColor = mixColor(blink, whiteColor, redColor);
+				currentColor = mixColor(blink, whiteColor, redColor);
 				break;
+		}
+		if (mTargetThreatLevel != mCurrentThreatLevel) {
+			float[] targetColor = whiteColor;
+			switch (mTargetThreatLevel) {
+				case NOT_MONITORING:
+					targetColor = greyColor;
+					break;
+				case PAST_THREAT:
+					targetColor = mixColor(blink, whiteColor, yellowColor);
+					break;
+				case LIVE_THREAT:
+					targetColor = mixColor(blink, whiteColor, redColor);
+					break;
 			}
 			currentColor = mixColor(mThreatLevelTransition, currentColor, targetColor);
 		}
 		GLES20.glUniform4fv(colorUniformLoc, 1, currentColor, 0);
 		GLES20.glUniform1i(textureUniformLoc, 0);
-		
+
 		// animate camera
 		Matrix.setIdentityM(mModelview, 0);
 		if (mCurrentThreatLevel == ThreatLevel.LIVE_THREAT || mTargetThreatLevel == ThreatLevel.LIVE_THREAT) {
@@ -239,27 +266,27 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 				delta = 1.0f - delta;
 			Matrix.translateM(mModelview, 0, 0.0f, -0.6f - 0.2f * delta, -1.6f - 0.4f * delta); // 0.0f, -0.8f, -2.0f
 			Matrix.rotateM(mModelview, 0, -85.0f + 5.0f * delta, 1.0f, 0.0f, 0.0f); // -80.0f
-			
+
 		} else {
 			Matrix.translateM(mModelview, 0, 0.0f, -0.6f, -1.6f);
 			Matrix.rotateM(mModelview, 0, -85.0f, 1.0f, 0.0f, 0.0f);
 		}
 		Matrix.multiplyMM(mMVP, 0, mProjection, 0, mModelview, 0);
 		GLES20.glUniformMatrix4fv(mvpUniformLoc, 1, false, mMVP, 0);
-		
+
 		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, androidTexture);
-		androidMesh.draw(mProgram);
-		
+		androidMesh.draw(mAnimatedProgram);
+
 		// restore color
 		GLES20.glUniform4fv(colorUniformLoc, 1, whiteColor, 0);
 
 		if (mCurrentThreatLevel == ThreatLevel.LIVE_THREAT || mTargetThreatLevel == ThreatLevel.LIVE_THREAT) {
 			// draw a bee rotating around the android
-			
+
 			float fadeIn = mThreatLevelTransition;
 			if (mTargetThreatLevel != ThreatLevel.LIVE_THREAT) fadeIn = 1.0f - fadeIn; // fade out
 			float beePositionZ = 2.0f * (1.0f - fadeIn) * (1.0f - fadeIn); // animate the bee going in/out
-			
+
 			final float beeSize = 0.2f;
 			Matrix.rotateM(mModelview, 0, (float)((-240.0 * animTime) % 360.0), 0.0f, 0.0f, 1.0f); // rotate around android
 			Matrix.translateM(mModelview, 0, 0.6f, 0.0f, 0.7f + 0.1f * (float)Math.sin(12.0 * animTime) + beePositionZ); // go up and down
@@ -267,11 +294,66 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 			Matrix.scaleM(mModelview, 0, beeSize, beeSize, beeSize);
 			Matrix.multiplyMM(mMVP, 0, mProjection, 0, mModelview, 0);
 			GLES20.glUniformMatrix4fv(mvpUniformLoc, 1, false, mMVP, 0);
-	
+
 			GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, beeTexture);
 			beeMesh.tick();
-			beeMesh.draw(mProgram);
+			beeMesh.draw(mAnimatedProgram);
+		}
+	}
+	
+	/**
+	 * Tries to render at 30 Hz (see bottom part)
+	 */
+	public void onDrawFrame(GL10 arg0) {
+		Context ctx = MainActivity.getInstance();
+
+		long timeMillis = System.currentTimeMillis() - mStartTimeMillis;
+		double animTime = 0.001 * (double)timeMillis; // in seconds
+		
+		// OpenGL drawing
+		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT | GLES20.GL_DEPTH_BUFFER_BIT);
+
+		updateAndroidAndBee();
+		drawAndroidAndBee(animTime);
+
+		GLES20.glDisable(GLES20.GL_DEPTH_TEST);
+		GLES20.glEnable(GLES20.GL_BLEND);
+		GLES20.glDisable(GLES20.GL_CULL_FACE);
+		GLES20.glBlendFunc(GLES20.GL_SRC_ALPHA, GLES20.GL_ONE_MINUS_SRC_ALPHA);
+		GLES20.glUseProgram(mTexturedProgram);
+		int resolutionUniformLoc = GLES20.glGetUniformLocation(mTexturedProgram, "resolution");
+		int textureUniformLoc = GLES20.glGetUniformLocation(mTexturedProgram, "colormap");
+		int colorUniformLoc = GLES20.glGetUniformLocation(mTexturedProgram, "color");
+		GLES20.glUniform2f(resolutionUniformLoc, 1024.0f, 1024.0f);
+		GLES20.glUniform1i(textureUniformLoc, 0);
+		GLES20.glUniform3f(colorUniformLoc, 1.0f, 1.0f, 1.0f);
+		String message = "???";
+		switch (mNextThreatLevel) {
+			case NOT_MONITORING:
+				message = ctx.getString(R.string.honeypot_not_monitoring);
+				break;
+			case NO_THREAT:
+				message = ctx.getString(R.string.honeypot_no_threat);
+				break;
+			case PAST_THREAT:
+				message = ctx.getString(R.string.honeypot_past_threat);
+				break;
+			case LIVE_THREAT:
+				message = ctx.getString(R.string.honeypot_live_threat);
+				break;
 		}
+		float textWidth = font.getTextWidth(message);
+		float textHeight = 40.0f;
+		float bubbleSize = textWidth + 100.0f;
+		float y = 0.75f * 1024.0f + 32.0f * (float)Math.sin(2.0*animTime);
+		float x = 0.5f * 1024.0f + 16.0f * (float)Math.cos(1.0*animTime);
+		drawTexturedQuad(speechBubbleTexture, x - 0.5f*bubbleSize, y - 0.25f*bubbleSize, bubbleSize, 0.5f*bubbleSize);
+		GLES20.glUniform3f(colorUniformLoc, 0.0f, 0.0f, 0.0f);
+		font.drawText(mTexturedProgram, message, x - 0.5f*textWidth,
+				y - 0.5f*textHeight);
+		GLES20.glUseProgram(0);
+		GLES20.glEnable(GLES20.GL_DEPTH_TEST);
+		GLES20.glDisable(GLES20.GL_BLEND);
 		
 		long deltaTime = System.currentTimeMillis() - mStartTimeMillis - timeMillis; // time for one frame
 		if (deltaTime < 33) {
@@ -297,7 +379,42 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		GLES20.glViewport(0, 0, width, height);
 	}
 	
-	// some private helper methods
+	// some helper functions
+	private void drawTexturedQuad(int texture, float x, float y, float w, float h) {
+		int vertexCount = 4;
+		int vertexSize = (2+2)*4; // size in bytes
+		FloatBuffer buffer = ByteBuffer.allocateDirect(vertexCount * vertexSize).order(ByteOrder.nativeOrder()).asFloatBuffer();
+		buffer.put(x); buffer.put(y);
+		buffer.put(0.0f); buffer.put(0.0f);
+		buffer.put(x+w); buffer.put(y);
+		buffer.put(1.0f); buffer.put(0.0f);
+		buffer.put(x); buffer.put(y+h);
+		buffer.put(0.0f); buffer.put(1.0f);
+		buffer.put(x+w); buffer.put(y+h);
+		buffer.put(1.0f); buffer.put(1.0f);
+		buffer.position(0); // rewind
+
+		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, texture);
+
+		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, mQuadVertexBuffer);
+		GLES20.glBufferData(GLES20.GL_ARRAY_BUFFER, vertexSize * vertexCount, buffer, GLES20.GL_STREAM_DRAW);
+
+		int positionIndex = GLES20.glGetAttribLocation(mTexturedProgram, "position");
+		int texCoordIndex = GLES20.glGetAttribLocation(mTexturedProgram, "texCoord");
+
+		GLES20.glEnableVertexAttribArray(positionIndex);
+		GLES20.glEnableVertexAttribArray(texCoordIndex);
+
+		GLES20.glVertexAttribPointer(positionIndex, 2, GLES20.GL_FLOAT, false, vertexSize, 0);
+		GLES20.glVertexAttribPointer(texCoordIndex, 2, GLES20.GL_FLOAT, false, vertexSize, 8);
+
+		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, vertexCount);
+
+		GLES20.glBindBuffer(GLES20.GL_ARRAY_BUFFER, 0);
+
+		GLES20.glDisableVertexAttribArray(positionIndex);
+		GLES20.glDisableVertexAttribArray(texCoordIndex);
+	}
 	
 	private float[] mixColor(float alpha, float[] color1, float[] color2) {
 		float[] color3 = new float[4];
@@ -308,7 +425,9 @@ public class ThreatIndicatorGLRenderer implements Renderer {
 		return color3;
 	}
 	
-	private int loadTexture(String filePath) {
+	public static int loadTexture(String filePath) {
+		AssetManager assets = MainActivity.getInstance().getAssets();
+
 		Bitmap bitmap;
 		try {
 			bitmap = BitmapFactory.decodeStream(assets.open(filePath));