public class ShaderAdapter extends CursorAdapter {
	private int idIndex;
	private int thumbIndex;
	private int nameIndex;
	private int modifiedIndex;
	private int textColorSelected;
	private int textColorUnselected;
	private long selectedShaderId;

	public ShaderAdapter(Context context, Cursor cursor) {
		super(context, cursor, false);

		indexColumns(cursor);
		initTextColors(context);
	}

	public void setSelectedId(long id) {
		selectedShaderId = id;
	}

	public String getName(int position) {
		Cursor cursor = (Cursor) getItem(position);
		return cursor != null ? cursor.getString(nameIndex) : null;
	}

	public String getTitle(Cursor cursor) {
		String title = cursor.getString(nameIndex);
		return title != null && title.length() > 0 ?
				title :
				cursor.getString(modifiedIndex);
	}

	@Override
	public View newView(Context context, Cursor cursor, ViewGroup parent) {
		LayoutInflater inflater = LayoutInflater.from(parent.getContext());
		return inflater.inflate(
				R.layout.row_shader,
				parent,
				false);
	}

	@Override
	public void bindView(View view, Context context, Cursor cursor) {
		ViewHolder holder = getViewHolder(view);
		setData(holder, cursor);

		holder.title.setTextColor(
				cursor.getLong(idIndex) == selectedShaderId ?
						textColorSelected :
						textColorUnselected);
	}

	ViewHolder getViewHolder(View view) {
		ViewHolder holder;

		if ((holder = (ViewHolder) view.getTag()) == null) {
			holder = new ViewHolder();
			holder.icon = view.findViewById(R.id.shader_icon);
			holder.title = view.findViewById(R.id.shader_title);
			view.setTag(holder);
		}

		return holder;
	}

	void setData(ViewHolder holder, Cursor cursor) {
		byte[] bytes = cursor.getBlob(thumbIndex);
		Bitmap bitmap = null;

		if (bytes != null && bytes.length > 0) {
			bitmap = BitmapFactory.decodeByteArray(
					bytes,
					0,
					bytes.length);
		}

		holder.icon.setImageBitmap(bitmap);
		holder.title.setText(getTitle(cursor));
	}

	private void indexColumns(Cursor cursor) {
		idIndex = cursor.getColumnIndex(
				Database.SHADERS_ID);
		thumbIndex = cursor.getColumnIndex(
				Database.SHADERS_THUMB);
		nameIndex = cursor.getColumnIndex(
				Database.SHADERS_NAME);
		modifiedIndex = cursor.getColumnIndex(
				Database.SHADERS_MODIFIED);
	}

	private static final class ViewHolder {
		private ImageView icon;
		private TextView title;
	}

	private void initTextColors(Context context) {
		textColorSelected = ContextCompat.getColor(
				context,
				R.color.accent);
		textColorUnselected = ContextCompat.getColor(
				context,
				R.color.drawer_text_unselected);
	}
} public class ShaderEditorApp extends Application {
	public static final Preferences preferences = new Preferences();
	public static final Database db = new Database();
	public static final UndoRedo.EditHistory editHistory =
			new UndoRedo.EditHistory();

	private static final BatteryLevelReceiver batteryLevelReceiver =
			new BatteryLevelReceiver();

	@Override
	public void onCreate() {
		super.onCreate();

		if (BuildConfig.DEBUG) {
			StrictMode.setThreadPolicy(
					new StrictMode.ThreadPolicy.Builder()
							.detectAll()
							.penaltyLog()
							.build());

			StrictMode.setVmPolicy(
					new StrictMode.VmPolicy.Builder()
							.detectLeakedSqlLiteObjects()
							.penaltyLog()
							.penaltyDeath()
							.build());
		}

		preferences.init(this);
		db.open(this);

		if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
			registerBatteryReceiver();
		}
	}

	private void registerBatteryReceiver() {
		IntentFilter filter = new IntentFilter();
		filter.addAction(Intent.ACTION_BATTERY_LOW);
		filter.addAction(Intent.ACTION_BATTERY_OKAY);
		filter.addAction(Intent.ACTION_BATTERY_CHANGED);
		registerReceiver(batteryLevelReceiver, filter);
		// Note it's not required to unregister the receiver because it
		// needs to be there as long as this application is running.
	}
} public class Database {
	public static final String FILE_NAME = "shaders.db";

	public static final String SHADERS = "shaders";
	public static final String SHADERS_ID = "_id";
	public static final String SHADERS_FRAGMENT_SHADER = "shader";
	public static final String SHADERS_THUMB = "thumb";
	public static final String SHADERS_NAME = "name";
	public static final String SHADERS_CREATED = "created";
	public static final String SHADERS_MODIFIED = "modified";
	public static final String SHADERS_QUALITY = "quality";

	public static final String TEXTURES = "textures";
	public static final String TEXTURES_ID = "_id";
	public static final String TEXTURES_NAME = "name";
	public static final String TEXTURES_WIDTH = "width";
	public static final String TEXTURES_HEIGHT = "height";
	public static final String TEXTURES_RATIO = "ratio";
	public static final String TEXTURES_THUMB = "thumb";
	public static final String TEXTURES_MATRIX = "matrix";

	private SQLiteDatabase db;
	private int textureThumbnailSize;

	public boolean importDatabase(Context context, String fileName) {
		SQLiteDatabase edb = null;
		try {
			edb = new ImportHelper(new ExternalDatabaseContext(context),
					fileName).getReadableDatabase();
			db.beginTransaction();
			if (importShaders(db, edb) && importTextures(db, edb)) {
				db.setTransactionSuccessful();
				return true;
			} else {
				return false;
			}
		} catch (SQLException e) {
			return false;
		} finally {
			if (db.inTransaction()) {
				db.endTransaction();
			}
			if (edb != null) {
				edb.close();
			}
		}
	}

	public void open(Context context) {
		textureThumbnailSize = Math.round(
				context.getResources().getDisplayMetrics().density * 48f);
		try {
			db = new OpenHelper(context).getWritableDatabase();
		} catch (SQLException e) {
			Toast.makeText(
					context,
					R.string.cannot_open_database,
					Toast.LENGTH_LONG).show();
		}
	}

	public boolean isOpen() {
		return db != null;
	}

	public static long getLong(Cursor cursor, String column) {
		int index = cursor.getColumnIndex(column);
		if (index < 0) {
			return 0L;
		}
		return cursor.getLong(index);
	}

	public static float getFloat(Cursor cursor, String column) {
		int index = cursor.getColumnIndex(column);
		if (index < 0) {
			return 0f;
		}
		return cursor.getFloat(index);
	}

	public static String getString(Cursor cursor, String column) {
		int index = cursor.getColumnIndex(column);
		if (index < 0) {
			return "";
		}
		return cursor.getString(index);
	}

	public static byte[] getBlob(Cursor cursor, String column) {
		int index = cursor.getColumnIndex(column);
		if (index < 0) {
			return null;
		}
		return cursor.getBlob(index);
	}

	public static boolean closeIfEmpty(Cursor cursor) {
		if (cursor != null && cursor.moveToFirst()) {
			return false;
		}

		if (cursor != null) {
			cursor.close();
		}

		return true;
	}

	public Cursor getShaders() {
		return db.rawQuery(
				"SELECT " +
						SHADERS_ID + "," +
						SHADERS_THUMB + "," +
						SHADERS_NAME + "," +
						SHADERS_MODIFIED +
						" FROM " + SHADERS +
						" ORDER BY " + SHADERS_ID,
				null);
	}

	public Cursor getTextures() {
		return db.rawQuery(
				"SELECT " +
						TEXTURES_ID + "," +
						TEXTURES_NAME + "," +
						TEXTURES_WIDTH + "," +
						TEXTURES_HEIGHT + "," +
						TEXTURES_THUMB +
						" FROM " + TEXTURES +
						" WHERE " + TEXTURES_RATIO + " = 1" +
						" ORDER BY " + TEXTURES_ID,
				null);
	}

	public Cursor getSamplerCubeTextures() {
		return db.rawQuery(
				"SELECT " +
						TEXTURES_ID + "," +
						TEXTURES_NAME + "," +
						TEXTURES_WIDTH + "," +
						TEXTURES_HEIGHT + "," +
						TEXTURES_THUMB +
						" FROM " + TEXTURES +
						" WHERE " + TEXTURES_RATIO + " = 1.5" +
						" ORDER BY " + TEXTURES_ID,
				null);
	}

	public boolean isShaderAvailable(long id) {
		Cursor cursor = db.rawQuery(
				"SELECT " +
						SHADERS_ID +
						" FROM " + SHADERS +
						" WHERE " + SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});
		if (cursor == null) {
			return false;
		}
		boolean exists = cursor.moveToFirst();
		cursor.close();
		return exists;
	}

	public Cursor getShader(long id) {
		return db.rawQuery(
				"SELECT " +
						SHADERS_ID + "," +
						SHADERS_FRAGMENT_SHADER + "," +
						SHADERS_NAME + "," +
						SHADERS_MODIFIED + "," +
						SHADERS_QUALITY +
						" FROM " + SHADERS +
						" WHERE " + SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	public Cursor getRandomShader() {
		return db.rawQuery(
				"SELECT " +
						SHADERS_ID + "," +
						SHADERS_FRAGMENT_SHADER + "," +
						SHADERS_MODIFIED + "," +
						SHADERS_QUALITY +
						" FROM " + SHADERS +
						" ORDER BY RANDOM() LIMIT 1",
				null);
	}

	public byte[] getThumbnail(long id) {
		Cursor cursor = db.rawQuery(
				"SELECT " +
						SHADERS_THUMB +
						" FROM " + SHADERS +
						" WHERE " + SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});

		if (closeIfEmpty(cursor)) {
			return null;
		}

		byte[] thumbnail = getBlob(cursor, SHADERS_THUMB);
		cursor.close();

		return thumbnail;
	}

	public long getFirstShaderId() {
		Cursor cursor = db.rawQuery(
				"SELECT " +
						SHADERS_ID +
						" FROM " + SHADERS +
						" ORDER BY " + SHADERS_ID +
						" LIMIT 1",
				null);

		if (closeIfEmpty(cursor)) {
			return 0;
		}

		long id = getLong(cursor, SHADERS_ID);
		cursor.close();

		return id;
	}

	public Cursor getTexture(long id) {
		return db.rawQuery(
				"SELECT " +
						TEXTURES_NAME + "," +
						TEXTURES_WIDTH + "," +
						TEXTURES_HEIGHT + "," +
						TEXTURES_MATRIX +
						" FROM " + TEXTURES +
						" WHERE " + TEXTURES_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	public Bitmap getTextureBitmap(String name) {
		Cursor cursor = db.rawQuery(
				"SELECT " +
						TEXTURES_MATRIX +
						" FROM " + TEXTURES +
						" WHERE " + TEXTURES_NAME + " = ?",
				new String[]{name});
		Bitmap bm = getTextureBitmap(cursor);
		cursor.close();
		return bm;
	}

	public Bitmap getTextureBitmap(Cursor cursor) {
		if (closeIfEmpty(cursor)) {
			return null;
		}
		return textureFromCursor(cursor);
	}

	public static long insertShader(
			SQLiteDatabase db,
			String shader,
			String name,
			String created,
			String modified,
			byte[] thumbnail,
			float quality) {
		ContentValues cv = new ContentValues();
		cv.put(SHADERS_FRAGMENT_SHADER, shader);
		cv.put(SHADERS_THUMB, thumbnail);
		cv.put(SHADERS_NAME, name);
		cv.put(SHADERS_CREATED, created);
		cv.put(SHADERS_MODIFIED, modified);
		cv.put(SHADERS_QUALITY, quality);
		return db.insert(SHADERS, null, cv);
	}

	public static long insertShader(
			SQLiteDatabase db,
			String shader,
			String name,
			byte[] thumbnail,
			float quality) {
		String now = currentTime();
		return insertShader(db, shader, name, now, now, thumbnail, quality);
	}

	public long insertShader(
			String shader,
			byte[] thumbnail,
			float quality) {
		return insertShader(db, shader, null, thumbnail, quality);
	}

	public long insertShader(
			Context context,
			String shader,
			String name) {
		return insertShader(db, shader, name,
				loadBitmapResource(context, R.drawable.thumbnail_new_shader),
				1f);
	}

	public long insertNewShader(Context context) {
		return insertShaderFromResource(
				context,
				null,
				R.raw.new_shader,
				R.drawable.thumbnail_new_shader,
				1f);
	}

	public long insertShaderFromResource(
			Context context,
			String name,
			int sourceId,
			int thumbId,
			float quality) {
		try {
			return insertShader(
					db,
					loadRawResource(context, sourceId),
					name,
					loadBitmapResource(context, thumbId),
					quality);
		} catch (IOException e) {
			return 0;
		}
	}

	public static long insertTexture(
			SQLiteDatabase db,
			String name,
			int width,
			int height,
			float ratio,
			byte[] thumb,
			byte[] matrix) {
		ContentValues cv = new ContentValues();
		cv.put(TEXTURES_NAME, name);
		cv.put(TEXTURES_WIDTH, width);
		cv.put(TEXTURES_HEIGHT, height);
		cv.put(TEXTURES_RATIO, ratio);
		cv.put(TEXTURES_THUMB, thumb);
		cv.put(TEXTURES_MATRIX, matrix);
		return db.insert(TEXTURES, null, cv);
	}

	public static long insertTexture(
			SQLiteDatabase db,
			String name,
			Bitmap bitmap,
			int thumbnailSize) {
		Bitmap thumbnail;

		try {
			thumbnail = Bitmap.createScaledBitmap(
					bitmap,
					thumbnailSize,
					thumbnailSize,
					true);
		} catch (IllegalArgumentException e) {
			return 0;
		}

		int w = bitmap.getWidth();
		int h = bitmap.getHeight();

		return insertTexture(
				db,
				name,
				w,
				h,
				calculateRatio(w, h),
				bitmapToPng(thumbnail),
				bitmapToPng(bitmap));
	}

	public long insertTexture(String name, Bitmap bitmap) {
		return insertTexture(
				db,
				name,
				bitmap,
				textureThumbnailSize);
	}

	public void updateShader(
			long id,
			String shader,
			byte[] thumbnail,
			float quality) {
		ContentValues cv = new ContentValues();
		cv.put(SHADERS_FRAGMENT_SHADER, shader);
		cv.put(SHADERS_MODIFIED, currentTime());
		cv.put(SHADERS_QUALITY, quality);

		if (thumbnail != null) {
			cv.put(SHADERS_THUMB, thumbnail);
		}

		db.update(SHADERS, cv, SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	public void updateShaderQuality(
			long id,
			float quality) {
		ContentValues cv = new ContentValues();
		cv.put(SHADERS_QUALITY, quality);

		db.update(SHADERS, cv, SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	public void updateShaderName(long id, String name) {
		ContentValues cv = new ContentValues();
		cv.put(SHADERS_NAME, name);
		db.update(SHADERS, cv, SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	public void removeShader(long id) {
		db.delete(SHADERS, SHADERS_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	public void removeTexture(long id) {
		db.delete(TEXTURES, TEXTURES_ID + " = ?",
				new String[]{String.valueOf(id)});
	}

	private static String currentTime() {
		return new SimpleDateFormat(
				"yyyy-MM-dd HH:mm:ss",
				Locale.US).format(new Date());
	}

	private static Bitmap textureFromCursor(Cursor cursor) {
		byte[] data = getBlob(cursor, TEXTURES_MATRIX);
		return BitmapFactory.decodeByteArray(data, 0, data.length);
	}

	private static String loadRawResource(
			Context context,
			int id) throws IOException {
		InputStream in = null;
		try {
			in = context.getResources().openRawResource(id);
			int l = in.available();
			byte[] b = new byte[l];
			return in.read(b) == l ? new String(b, "UTF-8") : null;
		} finally {
			if (in != null) {
				in.close();
			}
		}
	}

	private static byte[] loadBitmapResource(Context context, int id) {
		return bitmapToPng(BitmapFactory.decodeResource(
				context.getResources(),
				id));
	}

	private static byte[] bitmapToPng(Bitmap bitmap) {
		ByteArrayOutputStream out = new ByteArrayOutputStream();
		bitmap.compress(Bitmap.CompressFormat.PNG, 100, out);
		return out.toByteArray();
	}

	private static float calculateRatio(int width, int height) {
		// round to two decimal places to avoid problems with
		// rounding errors; the query will filter precisely 1 or 1.5
		return Math.round(((float) height / width) * 100f) / 100f;
	}

	private void createShadersTable(SQLiteDatabase db, Context context) {
		db.execSQL("DROP TABLE IF EXISTS " + SHADERS);
		db.execSQL("CREATE TABLE " + SHADERS + " (" +
				SHADERS_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
				SHADERS_FRAGMENT_SHADER + " TEXT NOT NULL," +
				SHADERS_THUMB + " BLOB," +
				SHADERS_NAME + " TEXT," +
				SHADERS_CREATED + " DATETIME," +
				SHADERS_MODIFIED + " DATETIME," +
				SHADERS_QUALITY + " REAL);");

		insertInitalShaders(db, context);
	}

	private static void addShadersQuality(SQLiteDatabase db) {
		db.execSQL("ALTER TABLE " + SHADERS +
				" ADD COLUMN " + SHADERS_QUALITY + " REAL;");
		db.execSQL("UPDATE " + SHADERS +
				" SET " + SHADERS_QUALITY + " = 1;");
	}

	private void insertInitalShaders(SQLiteDatabase db, Context context) {
		try {
			insertShader(
					db,
					loadRawResource(
							context,
							R.raw.default_shader),
					context.getString(R.string.default_shader),
					loadBitmapResource(
							context,
							R.drawable.thumbnail_default),
					1f);
		} catch (IOException e) {
			// shouldn't ever happen in production
			// and nothing can be done if it does
		}
	}

	private void createTexturesTable(SQLiteDatabase db, Context context) {
		db.execSQL("DROP TABLE IF EXISTS " + TEXTURES);
		db.execSQL("CREATE TABLE " + TEXTURES + " (" +
				TEXTURES_ID + " INTEGER PRIMARY KEY AUTOINCREMENT," +
				TEXTURES_NAME + " TEXT NOT NULL UNIQUE," +
				TEXTURES_WIDTH + " INTEGER," +
				TEXTURES_HEIGHT + " INTEGER," +
				TEXTURES_RATIO + " REAL," +
				TEXTURES_THUMB + " BLOB," +
				TEXTURES_MATRIX + " BLOB);");

		insertInitalTextures(db, context);
	}

	private static void addTexturesWidthHeightRatio(SQLiteDatabase db) {
		db.execSQL("ALTER TABLE " + TEXTURES +
				" ADD COLUMN " + TEXTURES_WIDTH + " INTEGER;");
		db.execSQL("ALTER TABLE " + TEXTURES +
				" ADD COLUMN " + TEXTURES_HEIGHT + " INTEGER;");
		db.execSQL("ALTER TABLE " + TEXTURES +
				" ADD COLUMN " + TEXTURES_RATIO + " REAL;");

		Cursor cursor = db.rawQuery(
				"SELECT " +
						TEXTURES_ID + "," +
						TEXTURES_MATRIX +
						" FROM " + TEXTURES,
				null);

		if (closeIfEmpty(cursor)) {
			return;
		}

		do {
			Bitmap bm = textureFromCursor(cursor);

			if (bm == null) {
				continue;
			}

			int width = bm.getWidth();
			int height = bm.getHeight();
			float ratio = calculateRatio(width, height);
			bm.recycle();

			db.execSQL("UPDATE " + TEXTURES +
					" SET " +
					TEXTURES_WIDTH + " = " + width + ", " +
					TEXTURES_HEIGHT + " = " + height + ", " +
					TEXTURES_RATIO + " = " + ratio +
					" WHERE " + TEXTURES_ID + " = " +
					getLong(cursor, TEXTURES_ID) + ";");

		} while (cursor.moveToNext());

		cursor.close();
	}

	private static void addShaderNames(SQLiteDatabase db) {
		db.execSQL("ALTER TABLE " + SHADERS +
				" ADD COLUMN " + SHADERS_NAME + " TEXT;");
	}

	private void insertInitalTextures(SQLiteDatabase db, Context context) {
		Database.insertTexture(
				db,
				context.getString(R.string.texture_name_noise),
				BitmapFactory.decodeResource(
						context.getResources(),
						R.drawable.texture_noise),
				textureThumbnailSize);
	}

	private static boolean importShaders(
			SQLiteDatabase dst,
			SQLiteDatabase src) {
		Cursor cursor = src.rawQuery(
				"SELECT *" +
						" FROM " + SHADERS +
						" ORDER BY " + SHADERS_ID,
				null);
		if (cursor == null) {
			return false;
		}
		int shaderIndex = cursor.getColumnIndex(SHADERS_FRAGMENT_SHADER);
		int thumbIndex = cursor.getColumnIndex(SHADERS_THUMB);
		int nameIndex = cursor.getColumnIndex(SHADERS_NAME);
		int createdIndex = cursor.getColumnIndex(SHADERS_CREATED);
		int modifiedIndex = cursor.getColumnIndex(SHADERS_MODIFIED);
		int qualityIndex = cursor.getColumnIndex(SHADERS_QUALITY);
		boolean success = true;
		if (cursor.moveToFirst()) {
			do {
				String createdDate = cursor.getString(createdIndex);
				String modifiedDate = cursor.getString(modifiedIndex);
				if (createdDate == null || modifiedDate == null ||
						shaderExists(dst, createdDate, modifiedDate)) {
					continue;
				}
				long shaderId = insertShader(dst,
						cursor.getString(shaderIndex),
						cursor.getString(nameIndex),
						createdDate,
						modifiedDate,
						cursor.getBlob(thumbIndex),
						cursor.getFloat(qualityIndex));
				if (shaderId < 1) {
					success = false;
					break;
				}
			} while (cursor.moveToNext());
		}
		cursor.close();
		return success;
	}

	private static boolean shaderExists(
			SQLiteDatabase db,
			String createdDate,
			String modifiedDate) {
		Cursor cursor = db.rawQuery(
				"SELECT " + SHADERS_ID +
						" FROM " + SHADERS +
						" WHERE " + SHADERS_CREATED + " = ?" +
						" AND " + SHADERS_MODIFIED + " = ?",
				new String[]{createdDate, modifiedDate});
		if (cursor == null) {
			return false;
		}
		boolean exists = cursor.moveToFirst() && cursor.getCount() > 0;
		cursor.close();
		return exists;
	}

	private static boolean importTextures(
			SQLiteDatabase dst,
			SQLiteDatabase src) {
		Cursor cursor = src.rawQuery(
				"SELECT *" +
						" FROM " + TEXTURES +
						" ORDER BY " + TEXTURES_ID,
				null);
		if (cursor == null) {
			return false;
		}
		int nameIndex = cursor.getColumnIndex(TEXTURES_NAME);
		int widthIndex = cursor.getColumnIndex(TEXTURES_WIDTH);
		int heightIndex = cursor.getColumnIndex(TEXTURES_HEIGHT);
		int ratioIndex = cursor.getColumnIndex(TEXTURES_RATIO);
		int thumbIndex = cursor.getColumnIndex(TEXTURES_THUMB);
		int matrixIndex = cursor.getColumnIndex(TEXTURES_MATRIX);
		boolean success = true;
		if (cursor.moveToFirst()) {
			do {
				String name = cursor.getString(nameIndex);
				if (name == null || textureExists(dst, name)) {
					continue;
				}
				long textureId = insertTexture(dst,
						name,
						cursor.getInt(widthIndex),
						cursor.getInt(heightIndex),
						cursor.getFloat(ratioIndex),
						cursor.getBlob(thumbIndex),
						cursor.getBlob(matrixIndex));
				if (textureId < 1) {
					success = false;
					break;
				}
			} while (cursor.moveToNext());
		}
		cursor.close();
		return success;
	}

	private static boolean textureExists(SQLiteDatabase db, String name) {
		Cursor cursor = db.rawQuery(
				"SELECT " + TEXTURES_ID +
						" FROM " + TEXTURES +
						" WHERE " + TEXTURES_NAME + " = ?",
				new String[]{name});
		if (cursor == null) {
			return false;
		}
		boolean exists = cursor.moveToFirst() && cursor.getCount() > 0;
		cursor.close();
		return exists;
	}

	private class OpenHelper extends SQLiteOpenHelper {
		private final Context context;

		@Override
		public void onCreate(SQLiteDatabase db) {
			createShadersTable(db, context);
			createTexturesTable(db, context);
		}

		@Override
		public void onDowngrade(
				SQLiteDatabase db,
				int oldVersion,
				int newVersion) {
			// without onDowngrade(), a downgrade will throw
			// an exception; can never happen in production
		}

		@Override
		public void onUpgrade(
				SQLiteDatabase db,
				int oldVersion,
				int newVersion) {
			if (oldVersion < 2) {
				createTexturesTable(db, context);
				insertInitalShaders(db, context);
			}

			if (oldVersion < 3) {
				addShadersQuality(db);
			}

			if (oldVersion < 4) {
				addTexturesWidthHeightRatio(db);
			}

			if (oldVersion < 5) {
				addShaderNames(db);
			}
		}

		private OpenHelper(Context context) {
			super(context, FILE_NAME, null, 5);
			this.context = context;
		}
	}

	private static class ImportHelper extends SQLiteOpenHelper {
		private ImportHelper(Context context, String path) {
			super(context, path, null, 1);
		}

		@Override
		public void onCreate(SQLiteDatabase db) {
			// do nothing
		}

		@Override
		public void onDowngrade(
				SQLiteDatabase db,
				int oldVersion,
				int newVersion) {
			// do nothing, but without that method we cannot open
			// different versions
		}

		@Override
		public void onUpgrade(
				SQLiteDatabase db,
				int oldVersion,
				int newVersion) {
			// do nothing, but without that method we cannot open
			// different versions
		}
	}

	// somehow it's required to use this ContextWrapper to access the
	// tables in an external database; without this, the database will
	// only contain the table "android_metadata"
	public static class ExternalDatabaseContext extends ContextWrapper {
		public ExternalDatabaseContext(Context base) {
			super(base);
		}

		@Override
		public File getDatabasePath(String name) {
			return new File(getFilesDir(), name);
		}

		@Override
		public SQLiteDatabase openOrCreateDatabase(String name, int mode,
				SQLiteDatabase.CursorFactory factory,
				DatabaseErrorHandler errorHandler) {
			return openOrCreateDatabase(name, mode, factory);
		}

		@Override
		public SQLiteDatabase openOrCreateDatabase(String name, int mode,
				SQLiteDatabase.CursorFactory factory) {
			return SQLiteDatabase.openOrCreateDatabase(
					getDatabasePath(name), null);
		}
	}
} public class EditorFragment extends Fragment {
	public static final String TAG = "EditorFragment";

	private ScrollView scrollView;
	private ShaderEditor shaderEditor;
	private UndoRedo undoRedo;
	private int yOffset;

	@Override
	public View onCreateView(
			LayoutInflater inflater,
			ViewGroup container,
			Bundle state) {
		View view = inflater.inflate(
				R.layout.fragment_editor,
				container,
				false);

		scrollView = view.findViewById(R.id.scroll_view);
		shaderEditor = view.findViewById(R.id.editor);
		undoRedo = new UndoRedo(shaderEditor, ShaderEditorApp.editHistory);

		Activity activity = getActivity();
		try {
			shaderEditor.setOnTextChangedListener(
					(ShaderEditor.OnTextChangedListener) activity);
		} catch (ClassCastException e) {
			throw new ClassCastException(activity.toString() +
					" must implement " +
					"ShaderEditor.OnTextChangedListener");
		}

		return view;
	}

	@Override
	public void onDestroyView() {
		super.onDestroyView();
		undoRedo.detachListener();
	}

	@Override
	public void onResume() {
		super.onResume();
		updateToPreferences();
		// only start listening after EditText restored its content
		// to make sure the initial change is not recorded
		undoRedo.listenForChanges();
	}

	public void undo() {
		undoRedo.undo();
	}

	public void redo() {
		undoRedo.redo();
	}

	public boolean hasErrorLine() {
		return shaderEditor.hasErrorLine();
	}

	public void clearError() {
		shaderEditor.setErrorLine(0);
	}

	public void updateHighlighting() {
		shaderEditor.updateHighlighting();
	}

	public void showError(String infoLog) {
		Activity activity = getActivity();
		if (activity == null) {
			return;
		}

		InfoLog.parse(infoLog);
		shaderEditor.setErrorLine(InfoLog.getErrorLine());
		updateHighlighting();

		Toast errorToast = Toast.makeText(
				activity,
				InfoLog.getMessage(),
				Toast.LENGTH_SHORT);
		errorToast.setGravity(
				Gravity.TOP | Gravity.CENTER_HORIZONTAL,
				0,
				getYOffset(activity));
		errorToast.show();
	}

	public boolean isModified() {
		return shaderEditor.isModified();
	}

	public String getText() {
		return shaderEditor.getCleanText();
	}

	public void setText(String text) {
		clearError();
		undoRedo.clearHistory();
		undoRedo.stopListeningForChanges();
		shaderEditor.setTextHighlighted(text);
		undoRedo.listenForChanges();
	}

	public void insertTab() {
		shaderEditor.insertTab();
	}

	public void addUniform(String name) {
		shaderEditor.addUniform(name);
	}

	public boolean isCodeVisible() {
		return scrollView.getVisibility() == View.VISIBLE;
	}

	public boolean toggleCode() {
		boolean visible = isCodeVisible();
		scrollView.setVisibility(visible ? View.GONE : View.VISIBLE);
		if (visible) {
			SoftKeyboard.hide(getActivity(), shaderEditor);
		}
		return visible;
	}

	private void updateToPreferences() {
		Preferences preferences = ShaderEditorApp.preferences;
		shaderEditor.setUpdateDelay(preferences.getUpdateDelay());
		shaderEditor.setTextSize(
				android.util.TypedValue.COMPLEX_UNIT_SP,
				preferences.getTextSize());
		shaderEditor.setTabWidth(preferences.getTabWidth());
	}

	private int getYOffset(Activity activity) {
		if (yOffset == 0) {
			float dp = getResources().getDisplayMetrics().density;

			try {
				ActionBar actionBar = ((AppCompatActivity) activity)
						.getSupportActionBar();

				if (actionBar != null) {
					yOffset = actionBar.getHeight();
				}
			} catch (ClassCastException e) {
				yOffset = Math.round(48f * dp);
			}

			yOffset += Math.round(16f * dp);
		}

		return yOffset;
	}
} public class ShaderRenderer implements GLSurfaceView.Renderer {
	public interface OnRendererListener {
		void onInfoLog(String error);

		void onFramesPerSecond(int fps);
	}

	public static final String UNIFORM_BACKBUFFER = "backbuffer";
	public static final String UNIFORM_BATTERY = "battery";
	public static final String UNIFORM_CAMERA_ADDENT = "cameraAddent";
	public static final String UNIFORM_CAMERA_BACK = "cameraBack";
	public static final String UNIFORM_CAMERA_FRONT = "cameraFront";
	public static final String UNIFORM_CAMERA_ORIENTATION = "cameraOrientation";
	public static final String UNIFORM_DATE = "date";
	public static final String UNIFORM_DAYTIME = "daytime";
	public static final String UNIFORM_FRAME_NUMBER = "frame";
	public static final String UNIFORM_FTIME = "ftime";
	public static final String UNIFORM_GRAVITY = "gravity";
	public static final String UNIFORM_GYROSCOPE = "gyroscope";
	public static final String UNIFORM_LIGHT = "light";
	public static final String UNIFORM_LINEAR = "linear";
	public static final String UNIFORM_MAGNETIC = "magnetic";
	public static final String UNIFORM_MOUSE = "mouse";
	public static final String UNIFORM_OFFSET = "offset";
	public static final String UNIFORM_ORIENTATION = "orientation";
	public static final String UNIFORM_INCLINATION = "inclination";
	public static final String UNIFORM_INCLINATION_MATRIX = "inclinationMatrix";
	public static final String UNIFORM_POINTERS = "pointers";
	public static final String UNIFORM_POINTER_COUNT = "pointerCount";
	public static final String UNIFORM_POSITION = "position";
	public static final String UNIFORM_POWER_CONNECTED = "powerConnected";
	public static final String UNIFORM_PRESSURE = "pressure";
	public static final String UNIFORM_PROXIMITY = "proximity";
	public static final String UNIFORM_RESOLUTION = "resolution";
	public static final String UNIFORM_ROTATION_MATRIX = "rotationMatrix";
	public static final String UNIFORM_ROTATION_VECTOR = "rotationVector";
	public static final String UNIFORM_SECOND = "second";
	public static final String UNIFORM_START_RANDOM = "startRandom";
	public static final String UNIFORM_SUB_SECOND = "subsecond";
	public static final String UNIFORM_TIME = "time";
	public static final String UNIFORM_TOUCH = "touch";

	private static final int[] TEXTURE_UNITS = {
			GLES20.GL_TEXTURE0,
			GLES20.GL_TEXTURE1,
			GLES20.GL_TEXTURE2,
			GLES20.GL_TEXTURE3,
			GLES20.GL_TEXTURE4,
			GLES20.GL_TEXTURE5,
			GLES20.GL_TEXTURE6,
			GLES20.GL_TEXTURE7,
			GLES20.GL_TEXTURE8,
			GLES20.GL_TEXTURE9,
			GLES20.GL_TEXTURE10,
			GLES20.GL_TEXTURE11,
			GLES20.GL_TEXTURE12,
			GLES20.GL_TEXTURE13,
			GLES20.GL_TEXTURE14,
			GLES20.GL_TEXTURE15,
			GLES20.GL_TEXTURE16,
			GLES20.GL_TEXTURE17,
			GLES20.GL_TEXTURE18,
			GLES20.GL_TEXTURE19,
			GLES20.GL_TEXTURE20,
			GLES20.GL_TEXTURE21,
			GLES20.GL_TEXTURE22,
			GLES20.GL_TEXTURE23,
			GLES20.GL_TEXTURE24,
			GLES20.GL_TEXTURE25,
			GLES20.GL_TEXTURE26,
			GLES20.GL_TEXTURE27,
			GLES20.GL_TEXTURE28,
			GLES20.GL_TEXTURE29,
			GLES20.GL_TEXTURE30,
			GLES20.GL_TEXTURE31};
	private static final int[] CUBE_MAP_TARGETS = {
			// all sides of a cube are stored in a single
			// rectangular source image for compactness:
			//
			//      +----+         +----+----+
			//     /| -Z |         | -X | -Z |
			// -X + +----+      >  +----+----+      +----+----+
			//    |/ -Y /               | -Y |      | -X | -Z |
			//    +----+                +----+      +----+----+
			//                                   >  | +Y | -Y |
			//       +----+        +----+           +----+----+
			//      / +Y /|        | +Y |           | +Z | +X |
			//     +----+ + +X  >  +----+----+      +----+----+
			//     | +Z |/         | +Z | +X |
			//     +----+          +----+----+
			//
			// so, from left to right, top to bottom:
			GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_X,
			GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_Z,
			GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_POSITIVE_Y,
			GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_NEGATIVE_Y,
			GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_POSITIVE_Z,
			GL11ExtensionPack.GL_TEXTURE_CUBE_MAP_POSITIVE_X};
	private static final float NS_PER_SECOND = 1000000000f;
	private static final long FPS_UPDATE_FREQUENCY_NS = 200000000L;
	private static final long BATTERY_UPDATE_INTERVAL = 10000000000L;
	private static final long DATE_UPDATE_INTERVAL = 1000000000L;
	private static final String SAMPLER_2D = "2D";
	private static final String SAMPLER_CUBE = "Cube";
	private static final String SAMPLER_EXTERNAL_OES = "ExternalOES";
	private static final Pattern PATTERN_SAMPLER = Pattern.compile(
			String.format(
					"uniform[ \t]+sampler(" +
							SAMPLER_2D + "|" +
							SAMPLER_CUBE + "|" +
							SAMPLER_EXTERNAL_OES +
							")+[ \t]+(%s);[ \t]*(.*)",
					AbstractSamplerPropertiesFragment.TEXTURE_NAME_PATTERN));
	private static final Pattern PATTERN_FTIME = Pattern.compile(
			"^#define[ \\t]+FTIME_PERIOD[ \\t]+([0-9\\.]+)[ \\t]*$",
			Pattern.MULTILINE);
	private static final Pattern PATTERN_VERSION = Pattern.compile(
			"^#version 3[0-9]{2} es$", Pattern.MULTILINE);
	private static final String OES_EXTERNAL =
			"#extension GL_OES_EGL_image_external : require\n";
	private static final String VERTEX_SHADER =
			"attribute vec2 position;" +
					"void main() {" +
					"gl_Position = vec4(position, 0., 1.);" +
					"}";
	private static final String VERTEX_SHADER_3 =
			"in vec2 position;" +
					"void main() {" +
					"gl_Position = vec4(position, 0., 1.);" +
					"}";
	private static final String FRAGMENT_SHADER =
			"#ifdef GL_FRAGMENT_PRECISION_HIGH\n" +
					"precision highp float;\n" +
					"#else\n" +
					"precision mediump float;\n" +
					"#endif\n" +
					"uniform vec2 resolution;" +
					"uniform sampler2D frame;" +
					"void main(void) {" +
					"gl_FragColor = texture2D(frame," +
					"gl_FragCoord.xy / resolution.xy).rgba;" +
					"}";

	private final TextureBinder textureBinder = new TextureBinder();
	private final ArrayList<String> textureNames = new ArrayList<>();
	private final ArrayList<TextureParameters> textureParameters =
			new ArrayList<>();
	private final BackBufferParameters backBufferTextureParams =
			new BackBufferParameters();
	private final int[] fb = new int[]{0, 0};
	private final int[] tx = new int[]{0, 0};
	private final int[] textureLocs = new int[32];
	private final int[] textureTargets = new int[32];
	private final int[] textureIds = new int[32];
	private final float[] surfaceResolution = new float[]{0, 0};
	private final float[] resolution = new float[]{0, 0};
	private final float[] touch = new float[]{0, 0};
	private final float[] mouse = new float[]{0, 0};
	private final float[] pointers = new float[30];
	private final float[] offset = new float[]{0, 0};
	private final float[] daytime = new float[]{0, 0, 0};
	private final float[] dateTime = new float[]{0, 0, 0, 0};
	private final float[] rotationMatrix = new float[9];
	private final float[] inclinationMatrix = new float[9];
	private final float[] orientation = new float[]{0, 0, 0};
	private final Context context;
	private final ByteBuffer vertexBuffer;

	private AccelerometerListener accelerometerListener;
	private CameraListener cameraListener;
	private GravityListener gravityListener;
	private GyroscopeListener gyroscopeListener;
	private MagneticFieldListener magneticFieldListener;
	private LightListener lightListener;
	private LinearAccelerationListener linearAccelerationListener;
	private PressureListener pressureListener;
	private ProximityListener proximityListener;
	private RotationVectorListener rotationVectorListener;
	private OnRendererListener onRendererListener;
	private String fragmentShader;
	private int version = 2;
	private int deviceRotation;
	private int surfaceProgram = 0;
	private int surfacePositionLoc;
	private int surfaceResolutionLoc;
	private int surfaceFrameLoc;
	private int program = 0;
	private int positionLoc;
	private int timeLoc;
	private int secondLoc;
	private int subSecondLoc;
	private int frameNumLoc;
	private int fTimeLoc;
	private int resolutionLoc;
	private int touchLoc;
	private int mouseLoc;
	private int pointerCountLoc;
	private int pointersLoc;
	private int powerConnectedLoc;
	private int gravityLoc;
	private int linearLoc;
	private int gyroscopeLoc;
	private int magneticLoc;
	private int rotationMatrixLoc;
	private int rotationVectorLoc;
	private int orientationLoc;
	private int inclinationMatrixLoc;
	private int inclinationLoc;
	private int lightLoc;
	private int pressureLoc;
	private int proximityLoc;
	private int offsetLoc;
	private int batteryLoc;
	private int dateTimeLoc;
	private int daytimeLoc;
	private int startRandomLoc;
	private int backBufferLoc;
	private int cameraOrientationLoc;
	private int cameraAddentLoc;
	private int numberOfTextures;
	private int pointerCount;
	private int frontTarget;
	private int backTarget = 1;
	private int frameNum;
	private long startTime;
	private long lastRender;
	private long lastBatteryUpdate;
	private long lastDateUpdate;
	private float batteryLevel;
	private float quality = 1f;
	private float startRandom;
	private float fTimeMax;
	private float[] gravityValues;
	private float[] linearValues;

	private volatile byte[] thumbnail = new byte[1];
	private volatile long nextFpsUpdate = 0;
	private volatile float sum;
	private volatile float samples;
	private volatile int lastFps;

	public ShaderRenderer(Context context) {
		this.context = context;

		// -1/1   1/1
		//    +---+
		//    |  /|
		//    | 0 |
		//    |/  |
		//    +---+
		// -1/-1  1/-1
		vertexBuffer = ByteBuffer.allocateDirect(8);
		vertexBuffer.put(new byte[]{
				-1, 1, // left top
				-1, -1, // left bottom
				1, 1, // right top, first triangle from last 3 vertices
				1, -1 // right bottom, second triangle from last 3 vertices
		}).position(0);
	}

	public void setVersion(int version) {
		this.version = version;
	}

	public void setFragmentShader(String source, float quality) {
		setQuality(quality);
		setFragmentShader(source);
	}

	private void setFragmentShader(String source) {
		fTimeMax = parseFTime(source);
		resetFps();
		fragmentShader = indexTextureNames(source);
	}

	public void setQuality(float quality) {
		this.quality = quality;
	}

	public void setOnRendererListener(OnRendererListener listener) {
		onRendererListener = listener;
	}

	@Override
	public void onSurfaceCreated(GL10 gl, EGLConfig config) {
		GLES20.glDisable(GLES20.GL_CULL_FACE);
		GLES20.glDisable(GLES20.GL_BLEND);
		GLES20.glDisable(GLES20.GL_DEPTH_TEST);

		GLES20.glClearColor(0f, 0f, 0f, 1f);

		if (surfaceProgram != 0) {
			// Don't glDeleteProgram(surfaceProgram) because
			// GLSurfaceView::onPause() destroys the GL context
			// what also deletes all programs.
			// With glDeleteProgram():
			// <core_glDeleteProgram:594>: GL_INVALID_VALUE
			surfaceProgram = 0;
		}

		if (program != 0) {
			// Don't glDeleteProgram(program);
			// same as above
			program = 0;
			deleteTargets();
		}

		if (fragmentShader != null && fragmentShader.length() > 0) {
			resetFps();
			createTextures();
			loadPrograms();
			indexLocations();
			enableAttribArrays();
			registerListeners();
		}
	}

	@Override
	public void onSurfaceChanged(GL10 gl, int width, int height) {
		startTime = lastRender = System.nanoTime();
		startRandom = (float) Math.random();
		frameNum = 0;

		surfaceResolution[0] = width;
		surfaceResolution[1] = height;
		deviceRotation = getDeviceRotation(context);

		float w = Math.round(width * quality);
		float h = Math.round(height * quality);

		if (w != resolution[0] || h != resolution[1]) {
			deleteTargets();
		}

		resolution[0] = w;
		resolution[1] = h;

		resetFps();
		openCameraListener();
	}

	@Override
	public void onDrawFrame(GL10 gl) {
		if (surfaceProgram == 0 || program == 0) {
			GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
					GLES20.GL_DEPTH_BUFFER_BIT);

			return;
		}

		GLES20.glUseProgram(program);
		GLES20.glVertexAttribPointer(positionLoc, 2, GLES20.GL_BYTE,
				false, 0, vertexBuffer);

		final long now = System.nanoTime();
		float delta = (now - startTime) / NS_PER_SECOND;

		if (timeLoc > -1) {
			GLES20.glUniform1f(timeLoc, delta);
		}
		if (secondLoc > -1) {
			GLES20.glUniform1i(secondLoc, (int) delta);
		}
		if (subSecondLoc > -1) {
			GLES20.glUniform1f(subSecondLoc, delta - (int) delta);
		}
		if (frameNumLoc > -1) {
			GLES20.glUniform1i(frameNumLoc, frameNum);
		}
		if (fTimeLoc > -1) {
			GLES20.glUniform1f(fTimeLoc,
					((delta % fTimeMax) / fTimeMax * 2f - 1f));
		}
		if (resolutionLoc > -1) {
			GLES20.glUniform2fv(resolutionLoc, 1, resolution, 0);
		}
		if (touchLoc > -1) {
			GLES20.glUniform2fv(touchLoc, 1, touch, 0);
		}
		if (mouseLoc > -1) {
			GLES20.glUniform2fv(mouseLoc, 1, mouse, 0);
		}
		if (pointerCountLoc > -1) {
			GLES20.glUniform1i(pointerCountLoc, pointerCount);
		}
		if (pointersLoc > -1) {
			GLES20.glUniform3fv(pointersLoc, pointerCount, pointers, 0);
		}
		if (gravityLoc > -1 && gravityValues != null) {
			GLES20.glUniform3fv(gravityLoc, 1, gravityValues, 0);
		}
		if (linearLoc > -1 && linearValues != null) {
			GLES20.glUniform3fv(linearLoc, 1, linearValues, 0);
		}
		if (gyroscopeLoc > -1 && gyroscopeListener != null) {
			GLES20.glUniform3fv(gyroscopeLoc, 1, gyroscopeListener.rotation, 0);
		}
		if (magneticLoc > -1 && magneticFieldListener != null) {
			GLES20.glUniform3fv(magneticLoc, 1, magneticFieldListener.values, 0);
		}
		if ((rotationMatrixLoc > -1 || orientationLoc > -1 ||
				inclinationMatrixLoc > -1 || inclinationLoc > -1) &&
				gravityValues != null) {
			setRotationMatrix();
		}
		if (lightLoc > -1 && lightListener != null) {
			GLES20.glUniform1f(lightLoc, lightListener.getAmbient());
		}
		if (pressureLoc > -1 && pressureListener != null) {
			GLES20.glUniform1f(pressureLoc, pressureListener.getPressure());
		}
		if (proximityLoc > -1 && proximityListener != null) {
			GLES20.glUniform1f(proximityLoc, proximityListener.getCentimeters());
		}
		if (rotationVectorLoc > -1 && rotationVectorListener != null) {
			GLES20.glUniform3fv(rotationVectorLoc, 1,
					rotationVectorListener.values, 0);
		}
		if (offsetLoc > -1) {
			GLES20.glUniform2fv(offsetLoc, 1, offset, 0);
		}
		if (batteryLoc > -1) {
			if (now - lastBatteryUpdate > BATTERY_UPDATE_INTERVAL) {
				// profiled getBatteryLevel() on slow/old devices
				// and it can take up to 6ms, so better do that
				// not for every frame but only once in a while
				batteryLevel = getBatteryLevel();
				lastBatteryUpdate = now;
			}
			GLES20.glUniform1f(batteryLoc, batteryLevel);
		}
		if (powerConnectedLoc > -1) {
			GLES20.glUniform1i(powerConnectedLoc,
					ShaderEditorApp.preferences.isPowerConnected() ? 1 : 0);
		}
		if (dateTimeLoc > -1 || daytimeLoc > -1) {
			if (now - lastDateUpdate > DATE_UPDATE_INTERVAL) {
				Calendar calendar = Calendar.getInstance();
				if (dateTimeLoc > -1) {
					dateTime[0] = calendar.get(Calendar.YEAR);
					dateTime[1] = calendar.get(Calendar.MONTH);
					dateTime[2] = calendar.get(Calendar.DAY_OF_MONTH);
					dateTime[3] = calendar.get(Calendar.HOUR_OF_DAY) * 3600f +
							calendar.get(Calendar.MINUTE) * 60f +
							calendar.get(Calendar.SECOND);
				}
				if (daytimeLoc > -1) {
					daytime[0] = calendar.get(Calendar.HOUR_OF_DAY);
					daytime[1] = calendar.get(Calendar.MINUTE);
					daytime[2] = calendar.get(Calendar.SECOND);
				}
				lastDateUpdate = now;
			}
			if (dateTimeLoc > -1) {
				GLES20.glUniform4fv(dateTimeLoc, 1, dateTime, 0);
			}
			if (daytimeLoc > -1) {
				GLES20.glUniform3fv(daytimeLoc, 1, daytime, 0);
			}
		}
		if (startRandomLoc > -1) {
			GLES20.glUniform1f(startRandomLoc, startRandom);
		}

		if (fb[0] == 0) {
			createTargets((int) resolution[0], (int) resolution[1]);
		}

		// first draw custom shader in framebuffer
		GLES20.glViewport(0, 0, (int) resolution[0], (int) resolution[1]);

		textureBinder.reset();

		if (backBufferLoc > -1) {
			textureBinder.bind(backBufferLoc, GLES20.GL_TEXTURE_2D,
					tx[backTarget]);
		}
		if (cameraListener != null) {
			if (cameraOrientationLoc > -1) {
				GLES20.glUniformMatrix2fv(cameraOrientationLoc, 1, false,
						cameraListener.getOrientationMatrix());
			}
			if (cameraAddentLoc > -1) {
				GLES20.glUniform2fv(cameraAddentLoc, 1,
						cameraListener.addent, 0);
			}
			cameraListener.update();
		}

		for (int i = 0; i < numberOfTextures; ++i) {
			textureBinder.bind(
					textureLocs[i],
					textureTargets[i],
					textureIds[i]);
		}

		GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb[frontTarget]);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

		// then draw framebuffer on screen
		GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
		GLES20.glViewport(0, 0,
				(int) surfaceResolution[0],
				(int) surfaceResolution[1]);
		GLES20.glUseProgram(surfaceProgram);
		GLES20.glVertexAttribPointer(surfacePositionLoc, 2, GLES20.GL_BYTE,
				false, 0, vertexBuffer);

		GLES20.glUniform2fv(surfaceResolutionLoc, 1, surfaceResolution, 0);

		GLES20.glUniform1i(surfaceFrameLoc, 0);
		GLES20.glActiveTexture(GLES20.GL_TEXTURE0);
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tx[frontTarget]);

		GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT);
		GLES20.glDrawArrays(GLES20.GL_TRIANGLE_STRIP, 0, 4);

		// swap buffers so the next image will be rendered
		// over the current backbuffer and the current image
		// will be the backbuffer for the next image
		int t = frontTarget;
		frontTarget = backTarget;
		backTarget = t;

		if (thumbnail == null) {
			thumbnail = saveThumbnail();
		}

		if (onRendererListener != null) {
			updateFps(now);
		}

		++frameNum;
	}

	public void unregisterListeners() {
		if (accelerometerListener != null) {
			accelerometerListener.unregister();
			gravityValues = linearValues = null;
		}

		if (gravityListener != null) {
			gravityListener.unregister();
			gravityValues = null;
		}

		if (linearAccelerationListener != null) {
			linearAccelerationListener.unregister();
			linearValues = null;
		}

		if (gyroscopeListener != null) {
			gyroscopeListener.unregister();
		}

		if (magneticFieldListener != null) {
			magneticFieldListener.unregister();
		}

		if (lightListener != null) {
			lightListener.unregister();
		}

		if (pressureListener != null) {
			pressureListener.unregister();
		}

		if (proximityListener != null) {
			proximityListener.unregister();
		}

		if (rotationVectorListener != null) {
			rotationVectorListener.unregister();
		}

		unregisterCameraListener();
	}

	public void touchAt(MotionEvent e) {
		float x = e.getX() * quality;
		float y = e.getY() * quality;

		touch[0] = x;
		touch[1] = resolution[1] - y;

		// to be compatible with http://glslsandbox.com/
		mouse[0] = x / resolution[0];
		mouse[1] = 1 - y / resolution[1];

		switch (e.getActionMasked()) {
			case MotionEvent.ACTION_UP:
			case MotionEvent.ACTION_CANCEL:
				pointerCount = 0;
				return;
		}

		pointerCount = Math.min(
				e.getPointerCount(),
				pointers.length / 3);

		for (int i = 0, offset = 0; i < pointerCount; ++i) {
			pointers[offset++] = e.getX(i) * quality;
			pointers[offset++] = resolution[1] - e.getY(i) * quality;
			pointers[offset++] = e.getTouchMajor(i);
		}
	}

	public void setOffset(float x, float y) {
		offset[0] = x;
		offset[1] = y;
	}

	public byte[] getThumbnail() {
		// settings thumbnail to null triggers
		// the capture on the OpenGL thread in
		// onDrawFrame()
		thumbnail = null;

		try {
			for (int trys = 10;
					trys-- > 0 && program > 0 && thumbnail == null; ) {
				Thread.sleep(100);
			}
		} catch (InterruptedException e) {
			Thread.currentThread().interrupt();
		}

		// don't clone() because the data doesn't need to be
		// protected from modification what means copying would
		// only mean using more memory than necessary
		return thumbnail;
	}

	private void resetFps() {
		sum = samples = 0;
		lastFps = 0;
		nextFpsUpdate = 0;
	}

	private void loadPrograms() {
		if (((surfaceProgram = Program.loadProgram(
				VERTEX_SHADER,
				FRAGMENT_SHADER)) == 0 ||
				(program = Program.loadProgram(
						getVertexShader(),
						fragmentShader)) == 0) &&
				onRendererListener != null) {
			onRendererListener.onInfoLog(Program.getInfoLog());
		}
	}

	private String getVertexShader() {
		Matcher m = PATTERN_VERSION.matcher(fragmentShader);
		if (version == 3 && m.find()) {
			return m.group(0) + "\n" + VERTEX_SHADER_3;
		} else {
			return VERTEX_SHADER;
		}
	}

	private void indexLocations() {
		surfacePositionLoc = GLES20.glGetAttribLocation(
				surfaceProgram, "position");
		surfaceResolutionLoc = GLES20.glGetUniformLocation(
				surfaceProgram, "resolution");
		surfaceFrameLoc = GLES20.glGetUniformLocation(
				surfaceProgram, "frame");

		positionLoc = GLES20.glGetAttribLocation(
				program, UNIFORM_POSITION);
		timeLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_TIME);
		secondLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_SECOND);
		subSecondLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_SUB_SECOND);
		frameNumLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_FRAME_NUMBER);
		fTimeLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_FTIME);
		resolutionLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_RESOLUTION);
		touchLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_TOUCH);
		mouseLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_MOUSE);
		pointerCountLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_POINTER_COUNT);
		pointersLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_POINTERS);
		powerConnectedLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_POWER_CONNECTED);
		gravityLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_GRAVITY);
		gyroscopeLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_GYROSCOPE);
		linearLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_LINEAR);
		magneticLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_MAGNETIC);
		rotationMatrixLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_ROTATION_MATRIX);
		rotationVectorLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_ROTATION_VECTOR);
		orientationLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_ORIENTATION);
		inclinationMatrixLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_INCLINATION_MATRIX);
		inclinationLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_INCLINATION);
		lightLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_LIGHT);
		pressureLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_PRESSURE);
		proximityLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_PROXIMITY);
		offsetLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_OFFSET);
		batteryLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_BATTERY);
		dateTimeLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_DATE);
		daytimeLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_DAYTIME);
		startRandomLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_START_RANDOM);
		backBufferLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_BACKBUFFER);
		cameraOrientationLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_CAMERA_ORIENTATION);
		cameraAddentLoc = GLES20.glGetUniformLocation(
				program, UNIFORM_CAMERA_ADDENT);

		for (int i = numberOfTextures; i-- > 0; ) {
			textureLocs[i] = GLES20.glGetUniformLocation(
					program,
					textureNames.get(i));
		}
	}

	private void enableAttribArrays() {
		GLES20.glEnableVertexAttribArray(surfacePositionLoc);
		GLES20.glEnableVertexAttribArray(positionLoc);
	}

	private void registerListeners() {
		if (gravityLoc > -1 || rotationMatrixLoc > -1 ||
				orientationLoc > -1 || inclinationMatrixLoc > -1 ||
				inclinationLoc > -1) {
			if (gravityListener == null) {
				gravityListener = new GravityListener(context);
			}
			if (!gravityListener.register()) {
				gravityListener = null;
				AccelerometerListener l = getAccelerometerListener();
				gravityValues = l != null ? l.gravity : null;
			} else {
				gravityValues = gravityListener.values;
			}
		}

		if (linearLoc > -1) {
			if (linearAccelerationListener == null) {
				linearAccelerationListener =
						new LinearAccelerationListener(context);
			}
			if (!linearAccelerationListener.register()) {
				linearAccelerationListener = null;
				AccelerometerListener l = getAccelerometerListener();
				linearValues = l != null ? l.linear : null;
			} else {
				linearValues = linearAccelerationListener.values;
			}
		}

		if (gyroscopeLoc > -1) {
			if (gyroscopeListener == null) {
				gyroscopeListener = new GyroscopeListener(context);
			}
			if (!gyroscopeListener.register()) {
				gyroscopeListener = null;
			}
		}

		if (magneticLoc > -1 || rotationMatrixLoc > -1 ||
				orientationLoc > -1 || inclinationMatrixLoc > -1 ||
				inclinationLoc > -1) {
			if (magneticFieldListener == null) {
				magneticFieldListener = new MagneticFieldListener(context);
			}
			if (!magneticFieldListener.register()) {
				magneticFieldListener = null;
			}
		}

		if (lightLoc > -1) {
			if (lightListener == null) {
				lightListener = new LightListener(context);
			}
			if (!lightListener.register()) {
				lightListener = null;
			}
		}

		if (pressureLoc > -1) {
			if (pressureListener == null) {
				pressureListener = new PressureListener(context);
			}
			if (!pressureListener.register()) {
				pressureListener = null;
			}
		}

		if (proximityLoc > -1) {
			if (proximityListener == null) {
				proximityListener = new ProximityListener(context);
			}
			if (!proximityListener.register()) {
				proximityListener = null;
			}
		}

		if (rotationVectorLoc > -1 || (magneticFieldListener == null &&
				(orientationLoc > -1 || rotationMatrixLoc > -1))) {
			if (rotationVectorListener == null) {
				rotationVectorListener = new RotationVectorListener(context);
			}
			if (!rotationVectorListener.register()) {
				rotationVectorListener = null;
			}
		}
	}

	private AccelerometerListener getAccelerometerListener() {
		if (accelerometerListener == null) {
			accelerometerListener = new AccelerometerListener(context);
		}
		if (!accelerometerListener.register()) {
			return null;
		}
		return accelerometerListener;
	}

	private byte[] saveThumbnail() {
		final int min = (int) Math.min(
				surfaceResolution[0],
				surfaceResolution[1]);
		final int pixels = min * min;
		final int[] rgba = new int[pixels];
		final int[] bgra = new int[pixels];
		final IntBuffer colorBuffer = IntBuffer.wrap(rgba);

		GLES20.glReadPixels(
				0,
				0,
				min,
				min,
				GLES20.GL_RGBA,
				GLES20.GL_UNSIGNED_BYTE,
				colorBuffer);

		for (int i = 0, e = pixels; i < pixels; ) {
			e -= min;

			for (int x = min, b = e; x-- > 0; ++i, ++b) {
				final int c = rgba[i];

				bgra[b] = ((c >> 16) & 0xff) |
						((c << 16) & 0xff0000) |
						(c & 0xff00ff00);
			}
		}

		try {
			ByteArrayOutputStream out = new ByteArrayOutputStream();
			Bitmap.createScaledBitmap(
					Bitmap.createBitmap(
							bgra,
							min,
							min,
							Bitmap.Config.ARGB_8888),
					144,
					144,
					true).compress(
					Bitmap.CompressFormat.PNG,
					100,
					out);

			return out.toByteArray();
		} catch (IllegalArgumentException e) {
			// will never happen because neither
			// width nor height <= 0
			return null;
		}
	}

	private void setRotationMatrix() {
		boolean haveInclination = false;
		if (gravityListener != null && magneticFieldListener != null &&
				SensorManager.getRotationMatrix(
						rotationMatrix,
						inclinationMatrix,
						gravityValues,
						magneticFieldListener.filtered)) {
			haveInclination = true;
		} else if (rotationVectorListener != null) {
			SensorManager.getRotationMatrixFromVector(
					rotationMatrix,
					rotationVectorListener.values);
		} else {
			return;
		}
		if (deviceRotation != 0) {
			int x = SensorManager.AXIS_Y;
			int y = SensorManager.AXIS_MINUS_X;
			switch (deviceRotation) {
				default:
					break;
				case 270:
					x = SensorManager.AXIS_MINUS_Y;
					y = SensorManager.AXIS_X;
					break;
			}
			SensorManager.remapCoordinateSystem(
					rotationMatrix,
					x,
					y,
					rotationMatrix);
		}
		if (rotationMatrixLoc > -1) {
			GLES20.glUniformMatrix3fv(rotationMatrixLoc, 1, true,
					rotationMatrix, 0);
		}
		if (orientationLoc > -1) {
			SensorManager.getOrientation(rotationMatrix, orientation);
			GLES20.glUniform3fv(orientationLoc, 1, orientation, 0);
		}
		if (inclinationMatrixLoc > -1 && haveInclination) {
			GLES20.glUniformMatrix3fv(inclinationMatrixLoc, 1, true,
					inclinationMatrix, 0);
		}
		if (inclinationLoc > -1 && haveInclination) {
			GLES20.glUniform1f(inclinationLoc,
					SensorManager.getInclination(inclinationMatrix));
		}
	}

	private void updateFps(long now) {
		long delta = now - lastRender;

		// because sum and samples are volatile
		synchronized (this) {
			sum += Math.min(NS_PER_SECOND / delta, 60f);

			if (++samples > 0xffff) {
				sum = sum / samples;
				samples = 1;
			}
		}

		if (now > nextFpsUpdate) {
			int fps = Math.round(sum / samples);
			if (fps != lastFps) {
				onRendererListener.onFramesPerSecond(fps);
				lastFps = fps;
			}
			nextFpsUpdate = now + FPS_UPDATE_FREQUENCY_NS;
		}

		lastRender = now;
	}

	private void deleteTargets() {
		if (fb[0] == 0) {
			return;
		}

		GLES20.glDeleteFramebuffers(2, fb, 0);
		GLES20.glDeleteTextures(2, tx, 0);

		fb[0] = 0;
	}

	private void createTargets(int width, int height) {
		deleteTargets();

		GLES20.glGenFramebuffers(2, fb, 0);
		GLES20.glGenTextures(2, tx, 0);

		createTarget(frontTarget, width, height, backBufferTextureParams);
		createTarget(backTarget, width, height, backBufferTextureParams);

		// unbind textures that were bound in createTarget()
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, 0);
		GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, 0);
	}

	private void createTarget(
			int idx,
			int width,
			int height,
			BackBufferParameters tp) {
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, tx[idx]);

		boolean useBitmap = tp.setPresetBitmap(width, height);
		if (!useBitmap) {
			GLES20.glTexImage2D(
					GLES20.GL_TEXTURE_2D,
					0,
					GLES20.GL_RGBA,
					width,
					height,
					0,
					GLES20.GL_RGBA,
					GLES20.GL_UNSIGNED_BYTE,
					null);
		}

		tp.setParameters(GLES20.GL_TEXTURE_2D);
		GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);

		GLES20.glBindFramebuffer(GLES20.GL_FRAMEBUFFER, fb[idx]);
		GLES20.glFramebufferTexture2D(
				GLES20.GL_FRAMEBUFFER,
				GLES20.GL_COLOR_ATTACHMENT0,
				GLES20.GL_TEXTURE_2D,
				tx[idx],
				0);

		if (!useBitmap) {
			// clear texture because some drivers
			// don't initialize texture memory
			GLES20.glClear(GLES20.GL_COLOR_BUFFER_BIT |
					GLES20.GL_DEPTH_BUFFER_BIT);
		}
	}

	private void deleteTextures() {
		if (textureIds[0] == 1 || numberOfTextures < 1) {
			return;
		}
		GLES20.glDeleteTextures(numberOfTextures, textureIds, 0);
	}

	private void createTextures() {
		deleteTextures();
		GLES20.glGenTextures(numberOfTextures, textureIds, 0);

		for (int i = 0; i < numberOfTextures; ++i) {
			String name = textureNames.get(i);
			if (UNIFORM_CAMERA_BACK.equals(name) ||
					UNIFORM_CAMERA_FRONT.equals(name)) {
				// handled in onSurfaceChanged() because we need
				// the dimensions of the surface to pick a preview
				// resolution
				continue;
			}

			Bitmap bitmap = ShaderEditorApp.db.getTextureBitmap(name);
			if (bitmap == null) {
				continue;
			}

			switch (textureTargets[i]) {
				default:
					continue;
				case GLES20.GL_TEXTURE_2D:
					createTexture(textureIds[i], bitmap,
							textureParameters.get(i));
					break;
				case GLES20.GL_TEXTURE_CUBE_MAP:
					createCubeTexture(textureIds[i], bitmap,
							textureParameters.get(i));
					break;
			}

			bitmap.recycle();
		}
	}

	private void createTexture(
			int id,
			Bitmap bitmap,
			TextureParameters tp) {
		GLES20.glBindTexture(GLES20.GL_TEXTURE_2D, id);
		tp.setParameters(GLES20.GL_TEXTURE_2D);
		TextureParameters.setBitmap(bitmap);
		GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_2D);
	}

	private void createCubeTexture(
			int id,
			Bitmap bitmap,
			TextureParameters tp) {
		GLES20.glBindTexture(GLES20.GL_TEXTURE_CUBE_MAP, id);
		tp.setParameters(GLES20.GL_TEXTURE_CUBE_MAP);

		int bitmapWidth = bitmap.getWidth();
		int bitmapHeight = bitmap.getHeight();
		int sideWidth = (int) Math.ceil(bitmapWidth / 2f);
		int sideHeight = Math.round(bitmapHeight / 3f);
		int sideLength = Math.min(sideWidth, sideHeight);
		int x = 0;
		int y = 0;

		for (int target : CUBE_MAP_TARGETS) {
			Bitmap side = Bitmap.createBitmap(
					bitmap,
					x,
					y,
					// cube textures need to be quadratic
					sideLength,
					sideLength,
					// don't flip cube textures
					null,
					true);

			GLUtils.texImage2D(
					target,
					0,
					GLES20.GL_RGBA,
					side,
					GLES20.GL_UNSIGNED_BYTE,
					0);

			side.recycle();

			if ((x += sideWidth) >= bitmapWidth) {
				x = 0;
				y += sideHeight;
			}
		}

		GLES20.glGenerateMipmap(GLES20.GL_TEXTURE_CUBE_MAP);
	}

	private void unregisterCameraListener() {
		if (cameraListener != null) {
			cameraListener.unregister();
			cameraListener = null;
		}
	}

	private void openCameraListener() {
		unregisterCameraListener();

		for (int i = 0; i < numberOfTextures; ++i) {
			String name = textureNames.get(i);
			if (UNIFORM_CAMERA_BACK.equals(name) ||
					UNIFORM_CAMERA_FRONT.equals(name)) {
				openCameraListener(name, textureIds[i],
						textureParameters.get(i));

				// only one camera can be opened at a time
				break;
			}
		}
	}

	private void openCameraListener(
			String name,
			int id,
			TextureParameters tp) {
		if (Build.VERSION.SDK_INT <
				Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
			return;
		}

		int cameraId = CameraListener.findCameraId(
				UNIFORM_CAMERA_BACK.equals(name) ?
						Camera.CameraInfo.CAMERA_FACING_BACK :
						Camera.CameraInfo.CAMERA_FACING_FRONT);

		if (cameraId < 0) {
			return;
		}

		if (cameraListener == null ||
				cameraListener.cameraId != cameraId) {
			unregisterCameraListener();
			requestCameraPermission();
			setCameraTextureProperties(id, tp);
			cameraListener = new CameraListener(
					id,
					cameraId,
					(int) resolution[0],
					(int) resolution[1],
					deviceRotation);
		}

		cameraListener.register();
	}

	private void requestCameraPermission() {
		String permission = android.Manifest.permission.CAMERA;
		if (ContextCompat.checkSelfPermission(context, permission) !=
				PackageManager.PERMISSION_GRANTED) {
			Activity activity;
			try {
				activity = (Activity) context;
			} catch (ClassCastException e) {
				return;
			}
			ActivityCompat.requestPermissions(
					activity,
					new String[]{permission},
					1);
		}
	}

	@TargetApi(Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1)
	private static void setCameraTextureProperties(
			int id,
			TextureParameters tp) {
		if (Build.VERSION.SDK_INT <
				Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
			return;
		}
		GLES20.glBindTexture(GLES11Ext.GL_TEXTURE_EXTERNAL_OES, id);
		tp.setParameters(GLES11Ext.GL_TEXTURE_EXTERNAL_OES);
	}

	private static float parseFTime(String source) {
		if (source != null) {
			Matcher m = PATTERN_FTIME.matcher(source);
			String s;
			if (m.find() && m.groupCount() > 0 &&
					(s = m.group(1)) != null) {
				return Float.parseFloat(s);
			}
		}
		return 3f;
	}

	private String indexTextureNames(String source) {
		if (source == null) {
			return null;
		}

		textureNames.clear();
		textureParameters.clear();
		numberOfTextures = 0;
		backBufferTextureParams.reset();

		final int maxTextures = textureIds.length;
		for (Matcher m = PATTERN_SAMPLER.matcher(source);
				m.find() && numberOfTextures < maxTextures; ) {
			String type = m.group(1);
			String name = m.group(2);
			String params = m.group(3);

			if (type == null || name == null) {
				continue;
			}

			if (UNIFORM_BACKBUFFER.equals(name)) {
				backBufferTextureParams.parse(params);
				continue;
			}

			int target;
			switch (type) {
				case SAMPLER_2D:
					target = GLES20.GL_TEXTURE_2D;
					break;
				case SAMPLER_CUBE:
					target = GLES20.GL_TEXTURE_CUBE_MAP;
					break;
				case SAMPLER_EXTERNAL_OES:
					if (Build.VERSION.SDK_INT >
							Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
						// needs to be done here or lint won't recognize
						// we're checking SDK version
						target = GLES11Ext.GL_TEXTURE_EXTERNAL_OES;
					} else {
						// ignore that uniform on lower SDKs
						continue;
					}
					if (!source.contains(OES_EXTERNAL)) {
						source = addPreprocessorDirective(source,
								OES_EXTERNAL);
					}
					break;
				default:
					continue;
			}

			textureTargets[numberOfTextures++] = target;
			textureNames.add(name);
			textureParameters.add(new TextureParameters(params));
		}

		return source;
	}

	private static String addPreprocessorDirective(String source,
			String directive) {
		// #version must always be the very first directive
		if (source.trim().startsWith("#version")) {
			int lf = source.indexOf("\n");
			if (lf < 0) {
				// no line break?
				return source;
			}
			++lf;
			return source.substring(0, lf) +
					directive +
					source.substring(lf);
		}
		return directive + source;
	}

	private float getBatteryLevel() {
		Intent batteryStatus = context.registerReceiver(
				null,
				new IntentFilter(Intent.ACTION_BATTERY_CHANGED));

		if (batteryStatus == null) {
			return 0;
		}

		int level = batteryStatus.getIntExtra(
				BatteryManager.EXTRA_LEVEL, -1);
		int scale = batteryStatus.getIntExtra(
				BatteryManager.EXTRA_SCALE, -1);

		return (float) level / scale;
	}

	private static int getDeviceRotation(Context context) {
		WindowManager wm = (WindowManager) context.getSystemService(
				Context.WINDOW_SERVICE);
		if (wm == null) {
			return 0;
		}
		switch (wm.getDefaultDisplay().getRotation()) {
			default:
			case Surface.ROTATION_0:
				return 0;
			case Surface.ROTATION_90:
				return 90;
			case Surface.ROTATION_180:
				return 180;
			case Surface.ROTATION_270:
				return 270;
		}
	}

	private static class TextureBinder {
		private int index;

		private void reset() {
			index = 0;
		}

		private void bind(int loc, int target, int textureId) {
			if (loc < 0 || index >= TEXTURE_UNITS.length) {
				return;
			}
			GLES20.glUniform1i(loc, index);
			GLES20.glActiveTexture(TEXTURE_UNITS[index]);
			GLES20.glBindTexture(target, textureId);
			++index;
		}
	}
} public class ShaderWallpaperService extends WallpaperService {
	private static ShaderWallpaperEngine engine;

	private ComponentName batteryLevelComponent;

	public static boolean isRunning() {
		return engine != null;
	}

	public static void setRenderMode(int renderMode) {
		if (engine != null) {
			engine.setRenderMode(renderMode);
		}
	}

	@Override
	public void onCreate() {
		super.onCreate();
		batteryLevelComponent = new ComponentName(this,
				BatteryLevelReceiver.class);
		enableComponent(batteryLevelComponent, true);
	}

	@Override
	public void onDestroy() {
		super.onDestroy();
		enableComponent(batteryLevelComponent, false);
		engine = null;
	}

	@Override
	public Engine onCreateEngine() {
		engine = new ShaderWallpaperEngine();
		return engine;
	}

	private class ShaderWallpaperEngine
			extends Engine
			implements SharedPreferences.OnSharedPreferenceChangeListener {
		private final Handler handler = new Handler();

		private ShaderWallpaperView view;

		@Override
		public void onSharedPreferenceChanged(
				SharedPreferences preferences,
				String key) {
			if (Preferences.WALLPAPER_SHADER.equals(key)) {
				setShader();
			}
		}

		@Override
		public void onCreate(SurfaceHolder holder) {
			super.onCreate(holder);
			view = new ShaderWallpaperView();
			setShader();
		}

		@Override
		public void onDestroy() {
			super.onDestroy();
			view.destroy();
			view = null;
		}

		@Override
		public void onVisibilityChanged(boolean visible) {
			super.onVisibilityChanged(visible);

			if (visible) {
				view.onResume();
			} else {
				view.onPause();
			}
		}

		@Override
		public void onTouchEvent(MotionEvent e) {
			super.onTouchEvent(e);
			view.getRenderer().touchAt(e);
		}

		@Override
		public void onOffsetsChanged(
				float xOffset,
				float yOffset,
				float xStep,
				float yStep,
				int xPixels,
				int yPixels) {
			view.getRenderer().setOffset(xOffset, yOffset);
		}

		private ShaderWallpaperEngine() {
			super();

			ShaderEditorApp.preferences.getSharedPreferences()
					.registerOnSharedPreferenceChangeListener(this);

			setTouchEventsEnabled(true);
		}

		private void setRenderMode(int renderMode) {
			if (view == null) {
				return;
			}

			view.setRenderMode(renderMode);
		}

		private void setShader() {
			if (!ShaderEditorApp.db.isOpen()) {
				handler.postDelayed(this::setShader, 100);

				return;
			}

			Cursor cursor = ShaderEditorApp.db.getShader(
					ShaderEditorApp.preferences.getWallpaperShader());

			boolean randomShader = false;

			while (cursor == null || !cursor.moveToFirst()) {
				if (cursor != null) {
					cursor.close();
				}

				if (randomShader) {
					return;
				}

				randomShader = true;
				cursor = ShaderEditorApp.db.getRandomShader();
			}

			if (randomShader) {
				ShaderEditorApp.preferences.setWallpaperShader(
						Database.getLong(cursor, Database.SHADERS_ID));
			}

			if (view != null) {
				view.getRenderer().setFragmentShader(
						Database.getString(cursor, Database.SHADERS_FRAGMENT_SHADER),
						Database.getFloat(cursor, Database.SHADERS_QUALITY));
			}

			cursor.close();
		}

		private class ShaderWallpaperView extends ShaderView {
			public ShaderWallpaperView() {
				super(ShaderWallpaperService.this,
						ShaderEditorApp.preferences.isBatteryLow() ?
								GLSurfaceView.RENDERMODE_WHEN_DIRTY :
								GLSurfaceView.RENDERMODE_CONTINUOUSLY);
			}

			@Override
			public final SurfaceHolder getHolder() {
				return ShaderWallpaperEngine.this.getSurfaceHolder();
			}

			public void destroy() {
				super.onDetachedFromWindow();
			}
		}
	}

	private void enableComponent(ComponentName name, boolean enable) {
		getPackageManager().setComponentEnabledSetting(name,
				enable ? PackageManager.COMPONENT_ENABLED_STATE_ENABLED :
						PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
				PackageManager.DONT_KILL_APP);
	}
} public class SoftKeyboard {
	public static void hide(Context context, View view) {
		if (context != null && view != null) {
			InputMethodManager imm = ((InputMethodManager) context.getSystemService(
					Context.INPUT_METHOD_SERVICE));
			if (imm != null) {
				imm.hideSoftInputFromWindow(view.getWindowToken(), 0);
			}
		}
	}
} public class SystemBarMetrics {
	public static void initSystemBars(AppCompatActivity activity) {
		initSystemBars(activity, null);
	}

	public static void initSystemBars(AppCompatActivity activity,
			Rect insets) {
		View mainLayout;
		if (activity != null &&
				(mainLayout = activity.findViewById(R.id.main_layout)) != null &&
				setSystemBarColor(
						activity.getWindow(),
						ShaderEditorApp.preferences.getSystemBarColor(),
						true)) {
			setWindowInsets(mainLayout, insets);
		}
	}

	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
	public static boolean setSystemBarColor(
			Window window,
			int color,
			boolean expand) {
		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
			return false;
		}
		if (expand) {
			window.getDecorView().setSystemUiVisibility(
					View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
							View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
							View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN);
		}
		window.setStatusBarColor(color);
		window.setNavigationBarColor(color);
		return true;
	}

	@TargetApi(Build.VERSION_CODES.LOLLIPOP)
	public static void hideNavigation(Window window) {
		if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
			return;
		}
		window.getDecorView().setSystemUiVisibility(
				View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY |
						View.SYSTEM_UI_FLAG_LAYOUT_STABLE |
						View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION |
						View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN |
						View.SYSTEM_UI_FLAG_HIDE_NAVIGATION |
						View.SYSTEM_UI_FLAG_FULLSCREEN);
	}

	public static int getToolBarHeight(Context context) {
		TypedValue tv = new TypedValue();
		return context.getTheme().resolveAttribute(
				android.R.attr.actionBarSize,
				tv,
				true) ? TypedValue.complexToDimensionPixelSize(
				tv.data,
				context.getResources().getDisplayMetrics()) : 0;
	}

	private static void setWindowInsets(final View mainLayout,
			final Rect windowInsets) {
		ViewCompat.setOnApplyWindowInsetsListener(mainLayout, (v, insets) -> {
			if (insets.hasSystemWindowInsets()) {
				int left = insets.getSystemWindowInsetLeft();
				int top = insets.getSystemWindowInsetTop();
				int right = insets.getSystemWindowInsetRight();
				int bottom = insets.getSystemWindowInsetBottom();
				mainLayout.setPadding(left, top, right, bottom);
				if (windowInsets != null) {
					windowInsets.set(left, top, right, bottom);
				}
			}
			return insets.consumeSystemWindowInsets();
		});
	}
} public class ShaderEditor extends AppCompatEditText {
	public interface OnTextChangedListener {
		void onTextChanged(String text);
	}

	private static final Pattern PATTERN_LINE = Pattern.compile(
			".*\\n");
	private static final Pattern PATTERN_NUMBERS = Pattern.compile(
			"\\b(\\d*[.]?\\d+)\\b");
	private static final Pattern PATTERN_PREPROCESSOR = Pattern.compile(
			"^[\t ]*(#define|#undef|#if|#ifdef|#ifndef|#else|#elif|#endif|" +
					"#error|#pragma|#extension|#version|#line)\\b",
			Pattern.MULTILINE);
	private static final Pattern PATTERN_KEYWORDS = Pattern.compile(
			"\\b(attribute|const|uniform|varying|break|continue|" +
					"do|for|while|if|else|in|out|inout|float|int|void|bool|true|false|" +
					"lowp|mediump|highp|precision|invariant|discard|return|mat2|mat3|" +
					"mat4|vec2|vec3|vec4|ivec2|ivec3|ivec4|bvec2|bvec3|bvec4|sampler2D|" +
					"samplerCube|struct|gl_Vertex|gl_FragCoord|gl_FragColor)\\b");
	private static final Pattern PATTERN_BUILTINS = Pattern.compile(
			"\\b(radians|degrees|sin|cos|tan|asin|acos|atan|pow|" +
					"exp|log|exp2|log2|sqrt|inversesqrt|abs|sign|floor|ceil|fract|mod|" +
					"min|max|clamp|mix|step|smoothstep|length|distance|dot|cross|" +
					"normalize|faceforward|reflect|refract|matrixCompMult|lessThan|" +
					"lessThanEqual|greaterThan|greaterThanEqual|equal|notEqual|any|all|" +
					"not|dFdx|dFdy|fwidth|texture2D|texture2DProj|texture2DLod|" +
					"texture2DProjLod|textureCube|textureCubeLod)\\b");
	private static final Pattern PATTERN_COMMENTS = Pattern.compile(
			"/\\*(?:.|[\\n\\r])*?\\*/|//.*");
	private static final Pattern PATTERN_TRAILING_WHITE_SPACE = Pattern.compile(
			"[\\t ]+$",
			Pattern.MULTILINE);
	private static final Pattern PATTERN_INSERT_UNIFORM = Pattern.compile(
			"^([ \t]*uniform.+)$",
			Pattern.MULTILINE);
	private static final Pattern PATTERN_ENDIF = Pattern.compile(
			"(#endif)\\b");
	private static final Pattern PATTERN_SHADER_TOY = Pattern.compile(
			".*void\\s+mainImage\\s*\\(.*");
	private static final Pattern PATTERN_MAIN = Pattern.compile(
			".*void\\s+main\\s*\\(.*");

	private final Handler updateHandler = new Handler();
	private final Runnable updateRunnable = new Runnable() {
		@Override
		public void run() {
			Editable e = getText();

			if (onTextChangedListener != null) {
				onTextChangedListener.onTextChanged(
						removeNonAscii(e.toString()));
			}

			highlightWithoutChange(e);
		}
	};

	private OnTextChangedListener onTextChangedListener;
	private int updateDelay = 1000;
	private int errorLine = 0;
	private boolean dirty = false;
	private boolean modified = true;
	private int colorError;
	private int colorNumber;
	private int colorKeyword;
	private int colorBuiltin;
	private int colorComment;
	private int tabWidthInCharacters = 0;
	private int tabWidth = 0;

	public static String removeNonAscii(String text) {
		return text.replaceAll("[^\\x0A\\x09\\x20-\\x7E]", "");
	}

	public ShaderEditor(Context context) {
		super(context);
		init(context);
	}

	public ShaderEditor(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context);
	}

	public void setOnTextChangedListener(OnTextChangedListener listener) {
		onTextChangedListener = listener;
	}

	public void setUpdateDelay(int ms) {
		updateDelay = ms;
	}

	public void setTabWidth(int characters) {
		if (tabWidthInCharacters == characters) {
			return;
		}

		tabWidthInCharacters = characters;
		tabWidth = Math.round(getPaint().measureText("m") * characters);
	}

	public boolean hasErrorLine() {
		return errorLine > 0;
	}

	public void setErrorLine(int line) {
		errorLine = line;
	}

	public void updateHighlighting() {
		highlightWithoutChange(getText());
	}

	public boolean isModified() {
		return dirty;
	}

	public void setTextHighlighted(CharSequence text) {
		if (text == null) {
			text = "";
		}

		cancelUpdate();

		errorLine = 0;
		dirty = false;

		modified = false;
		String src = removeNonAscii(text.toString());
		setText(highlight(new SpannableStringBuilder(src)));
		modified = true;

		if (onTextChangedListener != null) {
			onTextChangedListener.onTextChanged(src);
		}
	}

	public String getCleanText() {
		return PATTERN_TRAILING_WHITE_SPACE
				.matcher(getText())
				.replaceAll("");
	}

	public void insertTab() {
		int start = getSelectionStart();
		int end = getSelectionEnd();

		getText().replace(
				Math.min(start, end),
				Math.max(start, end),
				"\t",
				0,
				1);
	}

	public void addUniform(String statement) {
		if (statement == null) {
			return;
		}

		Editable e = getText();
		removeUniform(e, statement);

		Matcher m = PATTERN_INSERT_UNIFORM.matcher(e);
		int start = -1;

		while (m.find()) {
			start = m.end();
		}

		if (start > -1) {
			// add line break before statement because it's
			// inserted before the last line-break
			statement = "\n" + statement;
		} else {
			// add a line break after statement if there's no
			// uniform already
			statement += "\n";

			// add an empty line between the last #endif
			// and the now following uniform
			if ((start = endIndexOfLastEndIf(e)) > -1) {
				statement = "\n" + statement;
			}

			// move index past line break or to the start
			// of the text when no #endif was found
			++start;
		}

		e.insert(start, statement);
	}

	private void removeUniform(Editable e, String statement) {
		if (statement == null) {
			return;
		}

		String regex = "^(" + statement.replace(" ", "[ \\t]+");
		int p = regex.indexOf(";");
		if (p > -1) {
			regex = regex.substring(0, p);
		}
		regex += ".*\\n)$";

		Matcher m = Pattern.compile(regex, Pattern.MULTILINE).matcher(e);
		if (m.find()) {
			e.delete(m.start(), m.end());
		}
	}

	private int endIndexOfLastEndIf(Editable e) {
		Matcher m = PATTERN_ENDIF.matcher(e);
		int idx = -1;

		while (m.find()) {
			idx = m.end();
		}

		return idx;
	}

	private void init(Context context) {
		setHorizontallyScrolling(true);

		setFilters(new InputFilter[]{(source, start, end, dest, dstart, dend) -> {
			if (modified &&
					end - start == 1 &&
					start < source.length() &&
					dstart < dest.length()) {
				char c = source.charAt(start);

				if (c == '\n') {
					return autoIndent(source, dest, dstart, dend);
				}
			}

			return source;
		}});

		addTextChangedListener(new TextWatcher() {
			private int start = 0;
			private int count = 0;

			@Override
			public void onTextChanged(
					CharSequence s,
					int start,
					int before,
					int count) {
				this.start = start;
				this.count = count;
			}

			@Override
			public void beforeTextChanged(
					CharSequence s,
					int start,
					int count,
					int after) {
			}

			@Override
			public void afterTextChanged(Editable e) {
				cancelUpdate();
				convertTabs(e, start, count);

				String converted = convertShaderToySource(e.toString());
				if (converted != null) {
					setTextHighlighted(converted);
				}

				if (!modified) {
					return;
				}

				dirty = true;
				updateHandler.postDelayed(updateRunnable, updateDelay);
			}
		});

		setSyntaxColors(context);
		setUpdateDelay(ShaderEditorApp.preferences.getUpdateDelay());
		setTabWidth(ShaderEditorApp.preferences.getTabWidth());
	}

	private void setSyntaxColors(Context context) {
		colorError = ContextCompat.getColor(
				context,
				R.color.syntax_error);
		colorNumber = ContextCompat.getColor(
				context,
				R.color.syntax_number);
		colorKeyword = ContextCompat.getColor(
				context,
				R.color.syntax_keyword);
		colorBuiltin = ContextCompat.getColor(
				context,
				R.color.syntax_builtin);
		colorComment = ContextCompat.getColor(
				context,
				R.color.syntax_comment);
	}

	private void cancelUpdate() {
		updateHandler.removeCallbacks(updateRunnable);
	}

	private void highlightWithoutChange(Editable e) {
		modified = false;
		highlight(e);
		modified = true;
	}

	private Editable highlight(Editable e) {
		try {
			int length = e.length();

			// don't use e.clearSpans() because it will
			// remove too much
			clearSpans(e, length);

			if (length == 0) {
				return e;
			}

			if (errorLine > 0) {
				Matcher m = PATTERN_LINE.matcher(e);

				for (int i = errorLine; i-- > 0 && m.find(); ) {
					// {} because analyzers don't like for (); statements
				}

				e.setSpan(
						new BackgroundColorSpan(colorError),
						m.start(),
						m.end(),
						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			}

			if (ShaderEditorApp.preferences.disableHighlighting() &&
					length > 4096) {
				return e;
			}

			for (Matcher m = PATTERN_NUMBERS.matcher(e); m.find(); ) {
				e.setSpan(
						new ForegroundColorSpan(colorNumber),
						m.start(),
						m.end(),
						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			}

			for (Matcher m = PATTERN_PREPROCESSOR.matcher(e); m.find(); ) {
				e.setSpan(
						new ForegroundColorSpan(colorKeyword),
						m.start(),
						m.end(),
						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			}

			for (Matcher m = PATTERN_KEYWORDS.matcher(e); m.find(); ) {
				e.setSpan(
						new ForegroundColorSpan(colorKeyword),
						m.start(),
						m.end(),
						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			}

			for (Matcher m = PATTERN_BUILTINS.matcher(e); m.find(); ) {
				e.setSpan(
						new ForegroundColorSpan(colorBuiltin),
						m.start(),
						m.end(),
						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			}

			for (Matcher m = PATTERN_COMMENTS.matcher(e); m.find(); ) {
				e.setSpan(
						new ForegroundColorSpan(colorComment),
						m.start(),
						m.end(),
						Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
			}
		} catch (IllegalStateException ex) {
			// raised by Matcher.start()/.end() when
			// no successful match has been made what
			// shouldn't ever happen because of find()
		}

		return e;
	}

	private static void clearSpans(Editable e, int length) {
		// remove foreground color spans
		{
			ForegroundColorSpan[] spans = e.getSpans(
					0,
					length,
					ForegroundColorSpan.class);

			for (int i = spans.length; i-- > 0; ) {
				e.removeSpan(spans[i]);
			}
		}

		// remove background color spans
		{
			BackgroundColorSpan[] spans = e.getSpans(
					0,
					length,
					BackgroundColorSpan.class);

			for (int i = spans.length; i-- > 0; ) {
				e.removeSpan(spans[i]);
			}
		}
	}

	private CharSequence autoIndent(
			CharSequence source,
			Spanned dest,
			int dstart,
			int dend) {
		String indent = "";
		int istart = dstart - 1;

		// find start of this line
		boolean dataBefore = false;
		int pt = 0;

		for (; istart > -1; --istart) {
			char c = dest.charAt(istart);

			if (c == '\n') {
				break;
			}

			if (c != ' ' && c != '\t') {
				if (!dataBefore) {
					// indent always after those characters
					if (c == '{' ||
							c == '+' ||
							c == '-' ||
							c == '*' ||
							c == '/' ||
							c == '%' ||
							c == '^' ||
							c == '=') {
						--pt;
					}

					dataBefore = true;
				}

				// parenthesis counter
				if (c == '(') {
					--pt;
				} else if (c == ')') {
					++pt;
				}
			}
		}

		// copy indent of this line into the next
		if (istart > -1) {
			char charAtCursor = dest.charAt(dstart);
			int iend;

			for (iend = ++istart; iend < dend; ++iend) {
				char c = dest.charAt(iend);

				// auto expand comments
				if (charAtCursor != '\n' &&
						c == '/' &&
						iend + 1 < dend &&
						dest.charAt(iend) == c) {
					iend += 2;
					break;
				}

				if (c != ' ' && c != '\t') {
					break;
				}
			}

			indent += dest.subSequence(istart, iend);
		}

		// add new indent
		if (pt < 0) {
			indent += "\t";
		}

		// append white space of previous line and new indent
		return source + indent;
	}

	private void convertTabs(Editable e, int start, int count) {
		if (tabWidth < 1) {
			return;
		}

		String s = e.toString();

		for (int stop = start + count;
				(start = s.indexOf("\t", start)) > -1 && start < stop;
				++start) {
			e.setSpan(
					new TabWidthSpan(tabWidth),
					start,
					start + 1,
					Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
		}
	}

	private static String convertShaderToySource(String src) {
		if (!PATTERN_SHADER_TOY.matcher(src).find() ||
				PATTERN_MAIN.matcher(src).find()) {
			return null;
		}
		// Only include and translate uniforms that have an equivalent.
		return "#ifdef GL_FRAGMENT_PRECISION_HIGH\n" +
				"precision highp float;\n" +
				"#else\n" +
				"precision mediump float;\n" +
				"#endif\n\n" +
				"uniform vec2 resolution;\n" +
				"uniform float time;\n" +
				"uniform vec4 mouse;\n" +
				"uniform vec4 date;\n\n" +
				src.replaceAll("iResolution", "resolution")
						.replaceAll("iGlobalTime", "time")
						.replaceAll("iMouse", "mouse")
						.replaceAll("iDate", "date") +
				"\n\nvoid main() {\n" +
				"\tvec4 fragment_color;\n" +
				"\tmainImage(fragment_color, gl_FragCoord.xy);\n" +
				"\tgl_FragColor = fragment_color;\n" +
				"}\n";
	}

	private static class TabWidthSpan extends ReplacementSpan {
		private final int width;

		private TabWidthSpan(int width) {
			this.width = width;
		}

		@Override
		public int getSize(
				@NonNull Paint paint,
				CharSequence text,
				int start,
				int end,
				Paint.FontMetricsInt fm) {
			return width;
		}

		@Override
		public void draw(
				@NonNull Canvas canvas,
				CharSequence text,
				int start,
				int end,
				float x,
				int top,
				int y,
				int bottom,
				@NonNull Paint paint) {
		}
	}
} public class ShaderView extends GLSurfaceView {
	private ShaderRenderer renderer;

	public ShaderView(Context context, int renderMode) {
		super(context);
		init(context, renderMode);
	}

	public ShaderView(Context context) {
		super(context);
		init(context, GLSurfaceView.RENDERMODE_CONTINUOUSLY);
	}

	public ShaderView(Context context, AttributeSet attrs) {
		super(context, attrs);
		init(context, GLSurfaceView.RENDERMODE_CONTINUOUSLY);
	}

	@Override
	public void onPause() {
		super.onPause();
		renderer.unregisterListeners();
	}

	// click handling is implemented in renderer
	@SuppressLint("ClickableViewAccessibility")
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		renderer.touchAt(event);
		return true;
	}

	public void setFragmentShader(String src, float quality) {
		onPause();
		renderer.setFragmentShader(src, quality);
		onResume();
	}

	public ShaderRenderer getRenderer() {
		return renderer;
	}

	private void init(Context context, int renderMode) {
		renderer = new ShaderRenderer(context);

		// on some devices it's important to setEGLContextClientVersion()
		// even if the docs say it's not used when setEGLContextFactory()
		// is called; not doing so will crash the app (e.g. on the FP1)
		setEGLContextClientVersion(2);
		setEGLContextFactory(new ContextFactory(renderer));
		setRenderer(renderer);
		setRenderMode(renderMode);
	}

	private static class ContextFactory
			implements GLSurfaceView.EGLContextFactory {
		private final ShaderRenderer renderer;

		private ContextFactory(ShaderRenderer renderer) {
			this.renderer = renderer;
		}

		@Override
		public EGLContext createContext(EGL10 egl, EGLDisplay display,
				EGLConfig eglConfig) {
			int EGL_CONTEXT_CLIENT_VERSION = 0x3098;
			EGLContext context = egl.eglCreateContext(display, eglConfig,
					EGL10.EGL_NO_CONTEXT, new int[]{
							EGL_CONTEXT_CLIENT_VERSION,
							3,
							EGL10.EGL_NONE
					});
			if (context != null && context != EGL10.EGL_NO_CONTEXT &&
					context.getGL() != null) {
				renderer.setVersion(3);
				return context;
			}
			return egl.eglCreateContext(display, eglConfig,
					EGL10.EGL_NO_CONTEXT, new int[]{
							EGL_CONTEXT_CLIENT_VERSION,
							2,
							EGL10.EGL_NONE
					});
		}

		@Override
		public void destroyContext(EGL10 egl, EGLDisplay display,
				EGLContext context) {
			egl.eglDestroyContext(display, context);
		}
	}
} public class TouchThruDrawerLayout extends DrawerLayout {
	private boolean touchThru = false;

	public TouchThruDrawerLayout(Context context) {
		super(context);
	}

	public TouchThruDrawerLayout(Context context, AttributeSet attr) {
		super(context, attr);
	}

	public void setTouchThru(boolean touchThru) {
		this.touchThru = touchThru;
	}

	@Override
	public boolean onInterceptTouchEvent(MotionEvent event) {
		return !touchThru && super.onInterceptTouchEvent(event);
	}

	// click is handled in super class
	@SuppressLint("ClickableViewAccessibility")
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		return !touchThru && super.onTouchEvent(event);
	}
}
import android.annotation.SuppressLint;
import android.annotation.TargetApi;
import android.app.AlertDialog;
import android.content.ActivityNotFoundException;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.content.res.Configuration;
import android.database.Cursor;
import android.net.Uri;
import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v7.app.ActionBarDrawerToggle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.KeyEvent;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.Spinner;
import android.widget.Toast;
import java.io.IOException;
import java.io.InputStream;
import de.markusfisch.android.shadereditor.R;
import de.markusfisch.android.shadereditor.adapter.ShaderAdapter;
import de.markusfisch.android.shadereditor.app.ShaderEditorApp;
import de.markusfisch.android.shadereditor.database.Database;
import de.markusfisch.android.shadereditor.fragment.EditorFragment;
import de.markusfisch.android.shadereditor.opengl.ShaderRenderer;
import de.markusfisch.android.shadereditor.service.ShaderWallpaperService;
import de.markusfisch.android.shadereditor.view.SoftKeyboard;
import de.markusfisch.android.shadereditor.view.SystemBarMetrics;
import de.markusfisch.android.shadereditor.widget.ShaderEditor;
import de.markusfisch.android.shadereditor.widget.ShaderView;
import de.markusfisch.android.shadereditor.widget.TouchThruDrawerLayout;
					thumbnail,
					quality);
		} else {
			setToolbarTitle(ShaderEditorApp.db.insertShader(
					fragmentShader,
					thumbnail,
					quality));
		}

		// update thumbnails
		getShadersAsync();
	}

	private void toggleCode() {
		if (editorFragment != null) {
			drawerLayout.setTouchThru(editorFragment.toggleCode());
		}
	}

	private void addShader() {
		long id = ShaderEditorApp.preferences.getDefaultNewShader();
		if (id > 0) {
			if (!ShaderEditorApp.db.isShaderAvailable(id)) {
				ShaderEditorApp.preferences.setDefaultNewShader(0);
			} else {
				duplicateShader(id);
				return;
			}
		}
		selectShaderAndUpdate(ShaderEditorApp.db.insertNewShader(this));
	}

	private void duplicateShader(long id) {
		Cursor cursor = ShaderEditorApp.db.getShader(id);
		if (Database.closeIfEmpty(cursor)) {
			return;
		}
		selectShaderAndUpdate(ShaderEditorApp.db.insertShader(
				Database.getString(cursor, Database.SHADERS_FRAGMENT_SHADER),
				ShaderEditorApp.db.getThumbnail(id),
				Database.getFloat(cursor, Database.SHADERS_QUALITY)));
		cursor.close();
	}

	private void duplicateSelectedShader() {
		if (editorFragment == null || selectedShaderId < 1) {
			return;
		}

		if (editorFragment.isModified()) {
			saveShader(selectedShaderId);
		}

		selectShaderAndUpdate(ShaderEditorApp.db.insertShader(
				editorFragment.getText(),
				ShaderEditorApp.db.getThumbnail(selectedShaderId),
				quality));

		// update thumbnails
		getShadersAsync();
	}

	private void deleteShader(final long id) {
		if (id < 1) {
			return;
		}
		new AlertDialog.Builder(this)
				.setMessage(R.string.sure_remove_shader)
				.setPositiveButton(
						android.R.string.ok,
						(dialog, which) -> {
							ShaderEditorApp.db.removeShader(id);
							selectShaderAndUpdate(ShaderEditorApp
									.db.getFirstShaderId());
						})
				.setNegativeButton(android.R.string.cancel, null)
				.show();
	}

	private void shareShader() {
		if (editorFragment == null) {
			return;
		}

		String text = editorFragment.getText();
		if (!ShaderEditorApp.preferences.exportTabs() &&
				text.contains("\t")) {
			StringBuilder sb = new StringBuilder();
			for (int i = ShaderEditorApp.preferences.getTabWidth();
					i-- > 0; ) {
				sb.append(" ");
			}
			text = text.replaceAll("\t", sb.toString());
		}

		Intent intent = new Intent();
		intent.setType("text/plain");
		intent.setAction(Intent.ACTION_SEND);
		intent.putExtra(Intent.EXTRA_TEXT, text);

		startActivity(Intent.createChooser(
				intent,
				getString(R.string.share_shader)));
	}

	private void updateWallpaper(long id) {
		if (id < 1) {
			return;
		}

		if (editorFragment != null && editorFragment.isModified()) {
			saveShader(id);
		}

		// the onSharedPreferenceChanged() listener in WallpaperService
		// is only triggered if the value has changed so force this
		ShaderEditorApp.preferences.setWallpaperShader(0);
		ShaderEditorApp.preferences.setWallpaperShader(id);

		int message;
		if (ShaderWallpaperService.isRunning()) {