package ca.josephroque.bowlingcompanion.utilities;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.Paint;
import android.net.Uri;
import android.provider.MediaStore;
import android.util.Log;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import ca.josephroque.bowlingcompanion.Constants;
import ca.josephroque.bowlingcompanion.MainActivity;
import ca.josephroque.bowlingcompanion.database.Contract.FrameEntry;
import ca.josephroque.bowlingcompanion.database.Contract.GameEntry;
import ca.josephroque.bowlingcompanion.database.DatabaseHelper;
/**
* Created by Joseph Roque on 15-03-26. Provides methods relating to creating images of the statistics managed by the
* application.
*/
@SuppressWarnings("CheckStyle")
final class ImageUtils {
/** Identifies output from this class in Logcat. */
@SuppressWarnings("unused")
private static final String TAG = "ImageUtils";
/** Width of a single game's frames. */
private static final int BITMAP_GAME_WIDTH = 660;
/** Height of a single game's row. */
private static final int BITMAP_GAME_HEIGHT = 60;
/** Height of a single ball cell. */
private static final int BITMAP_GAME_BALL_HEIGHT = 20;
/** Width of a single ball cell. */
private static final int BITMAP_GAME_BALL_WIDTH = 20;
/** Height of a single frame cell. */
private static final int BITMAP_GAME_FRAME_HEIGHT = 40;
/** Width of a single frame cell. */
private static final int BITMAP_GAME_FRAME_WIDTH = 60;
/** Width of the cell for the game's name. */
private static final int BITMAP_SERIES_GAME_NAME_WIDTH = 80;
/** Default font size for writing game data. */
private static final float GAME_DEFAULT_FONT_SIZE = 12;
/** Small font size for writing game data. */
private static final float GAME_SMALL_FONT_SIZE = 8;
/** Large font size for writing game data. */
private static final float GAME_LARGE_FONT_SIZE = 16;
/** Y position of text for ball data. */
private static final float BALL_TEXT_Y = BITMAP_GAME_BALL_HEIGHT / 2
+ GAME_DEFAULT_FONT_SIZE / 2;
/**
* Creates an image, writing the game scores and individual ball values in a standard format.
*
* @param pinState state of the pins of each ball in each frame
* @param fouls indicates whether a foul was invoked for each ball
* @param gameScore final score of the game
* @param isManual indicates if the score of the game was manually set
* @return a formatted bitmap containing the game data
*/
@SuppressWarnings("UnusedAssignment") // canvas set to null to free memory
private static Bitmap createImageFromGame(boolean[][][] pinState,
boolean[][] fouls,
short gameScore,
boolean isManual) {
Bitmap bitmap =
Bitmap.createBitmap(BITMAP_GAME_WIDTH, BITMAP_GAME_HEIGHT, Bitmap.Config.RGB_565);
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.WHITE);
Paint paintBlackOutline = new Paint();
paintBlackOutline.setColor(Color.BLACK);
Paint paintText = new Paint();
paintText.setColor(Color.BLACK);
paintText.setTextAlign(Paint.Align.CENTER);
paintText.setTextSize(GAME_DEFAULT_FONT_SIZE);
int[] frameScores = new int[Constants.NUMBER_OF_FRAMES];
int foulCount = 0;
for (int frame = Constants.LAST_FRAME; frame >= 0; frame--) {
final String[] ballString = new String[3];
// Treat last frame differently than rest
if (frame == Constants.LAST_FRAME)
{
if (Arrays.equals(pinState[frame][0], Constants.FRAME_PINS_DOWN)) {
// If first ball is a strike, next two can be strikes/spares
ballString[0] = Constants.BALL_STRIKE;
if (Arrays.equals(pinState[frame][1], Constants.FRAME_PINS_DOWN)) {
ballString[1] = Constants.BALL_STRIKE;
ballString[2] = Score.getValueOfBall(pinState[frame][2], 2, true, false);
} else {
ballString[1] = Score.getValueOfBall(pinState[frame][1], 1, false, false);
if (Arrays.equals(pinState[frame][2], Constants.FRAME_PINS_DOWN))
ballString[2] = Constants.BALL_SPARE;
else
ballString[2] = Score.getValueOfBallDifference(
pinState[frame], 2, false, false);
}
} else {
// If first ball is not a strike, score is calculated normally
ballString[0] = Score.getValueOfBall(pinState[frame][0], 0, false, false);
if (Arrays.equals(pinState[frame][1], Constants.FRAME_PINS_DOWN)) {
ballString[1] = Constants.BALL_SPARE;
ballString[2] = Score.getValueOfBall(pinState[frame][2], 2, true, false);
} else {
ballString[1] =
Score.getValueOfBallDifference(pinState[frame], 1, false, false);
ballString[2] =
Score.getValueOfBallDifference(pinState[frame], 2, false, false);
}
}
} else {
ballString[0] = Score.getValueOfBallDifference(pinState[frame], 0, false, false);
if (!Arrays.equals(pinState[frame][0], Constants.FRAME_PINS_DOWN)) {
if (Arrays.equals(pinState[frame][1], Constants.FRAME_PINS_DOWN)) {
ballString[1] = Constants.BALL_SPARE;
ballString[2] =
Score.getValueOfBallDifference(pinState[frame + 1], 0, false, true);
} else {
ballString[1] =
Score.getValueOfBallDifference(pinState[frame], 1, false, false);
ballString[2] =
Score.getValueOfBallDifference(pinState[frame], 2, false, false);
}
} else {
ballString[1] =
Score.getValueOfBallDifference(pinState[frame + 1], 0, false, true);
if (Arrays.equals(pinState[frame + 1][0], Constants.FRAME_PINS_DOWN)) {
if (frame < Constants.LAST_FRAME - 1) {
ballString[2] = Score.getValueOfBallDifference(
pinState[frame + 2], 0, false, true);
} else {
ballString[2]
= Score.getValueOfBall(pinState[frame + 1][1], 1, false, true);
}
} else {
ballString[2] =
Score.getValueOfBallDifference(pinState[frame + 1], 1, false, true);
}
}
}
canvas.drawText(ballString[0], BITMAP_GAME_BALL_WIDTH / 2 + BITMAP_GAME_FRAME_WIDTH
* frame, BALL_TEXT_Y, paintText);
canvas.drawText(ballString[1], BITMAP_GAME_BALL_WIDTH + BITMAP_GAME_BALL_WIDTH / 2
+ BITMAP_GAME_FRAME_WIDTH * frame, BALL_TEXT_Y, paintText);
canvas.drawText(ballString[2], BITMAP_GAME_BALL_WIDTH * 2 + BITMAP_GAME_BALL_WIDTH / 2
+ BITMAP_GAME_FRAME_WIDTH * frame, BALL_TEXT_Y, paintText);
paintText.setTextSize(GAME_SMALL_FONT_SIZE);
for (int ball = 0; ball < pinState[frame].length; ball++) {
if (fouls[frame][ball]) {
foulCount++;
canvas.drawText("F", BITMAP_GAME_BALL_WIDTH * ball + BITMAP_GAME_FRAME_WIDTH
* frame + BITMAP_GAME_BALL_WIDTH / 2, BITMAP_GAME_BALL_HEIGHT
+ GAME_SMALL_FONT_SIZE + 2, paintText);
}
canvas.drawLine(BITMAP_GAME_BALL_WIDTH * ball + BITMAP_GAME_FRAME_WIDTH * frame,
0, BITMAP_GAME_BALL_WIDTH * ball + BITMAP_GAME_FRAME_WIDTH * frame,
BITMAP_GAME_BALL_HEIGHT, paintBlackOutline);
}
canvas.drawLine(BITMAP_GAME_FRAME_WIDTH * frame, BITMAP_GAME_BALL_HEIGHT,
BITMAP_GAME_FRAME_WIDTH * frame, BITMAP_GAME_FRAME_HEIGHT
+ BITMAP_GAME_BALL_HEIGHT, paintBlackOutline);
paintText.setTextSize(GAME_DEFAULT_FONT_SIZE);
if (!isManual) {
if (frame == Constants.LAST_FRAME) {
for (int b = 2; b >= 0; b--) {
switch (b) {
case 2:
frameScores[frame] += Score.getValueOfFrame(pinState[frame][b], true);
break;
case 1:
case 0:
if (Arrays.equals(pinState[frame][b], Constants.FRAME_PINS_DOWN)) {
frameScores[frame] += Score.getValueOfFrame(pinState[frame][b], true);
}
break;
default:
// do nothing
}
}
} else {
for (int b = 0; b < 3; b++) {
if (b < 2 && Arrays.equals(pinState[frame][b], Constants.FRAME_PINS_DOWN)) {
frameScores[frame] += Score.getValueOfFrame(pinState[frame][b], true);
frameScores[frame] += Score.getValueOfFrame(pinState[frame + 1][0], true);
if (b == 0) {
if (frame == Constants.LAST_FRAME - 1) {
if (frameScores[frame] == 30) {
frameScores[frame] +=
Score.getValueOfFrame(pinState[frame + 1][1], true);
} else {
frameScores[frame] += Score.getValueOfFrameDifference(
pinState[frame + 1][0], pinState[frame + 1][1]);
}
} else if (frameScores[frame] < 30) {
frameScores[frame] += Score.getValueOfFrameDifference(
pinState[frame + 1][0], pinState[frame + 1][1]);
} else {
frameScores[frame] += Score.getValueOfFrame(
pinState[frame + 2][0], true);
}
}
break;
} else if (b == 2) {
frameScores[frame] += Score.getValueOfFrame(pinState[frame][b], true);
}
}
}
}
}
int totalScore = 0;
paintText.setTextSize(GAME_LARGE_FONT_SIZE);
for (int i = 0; i < frameScores.length; i++) {
totalScore += frameScores[i];
canvas.drawText((!isManual)
? String.valueOf(totalScore)
: "--",
i * BITMAP_GAME_FRAME_WIDTH + BITMAP_GAME_FRAME_WIDTH / 2,
BITMAP_GAME_HEIGHT - 8,
paintText);
}
int scoreWithFouls = totalScore - 15 * foulCount;
if (scoreWithFouls < 0)
scoreWithFouls = 0;
canvas.drawText((!isManual)
? String.valueOf(scoreWithFouls)
: String.valueOf(gameScore),
BITMAP_GAME_WIDTH - BITMAP_GAME_FRAME_WIDTH / 2,
BITMAP_GAME_HEIGHT / 2 + GAME_LARGE_FONT_SIZE / 2,
paintText);
canvas.drawLines(new float[]{
0,
0,
BITMAP_GAME_WIDTH,
0,
0,
0,
0,
BITMAP_GAME_HEIGHT,
0,
BITMAP_GAME_HEIGHT - 1,
BITMAP_GAME_WIDTH,
BITMAP_GAME_HEIGHT - 1,
BITMAP_GAME_WIDTH - 1,
0,
BITMAP_GAME_WIDTH - 1,
BITMAP_GAME_HEIGHT,
0,
BITMAP_GAME_BALL_HEIGHT,
BITMAP_GAME_WIDTH - BITMAP_GAME_FRAME_WIDTH,
BITMAP_GAME_BALL_HEIGHT,
BITMAP_GAME_FRAME_WIDTH * Constants.NUMBER_OF_FRAMES,
0,
BITMAP_GAME_FRAME_WIDTH * Constants.NUMBER_OF_FRAMES,
BITMAP_GAME_HEIGHT
},
paintBlackOutline);
canvas = null;
return bitmap;
}
/**
* Loads the data of each game in a series and creates a single image to.
*
* @param context context used to get an instance of the database
* @param seriesId id of the series to load game from
* @return an image of each games' data and score
*/
@SuppressWarnings("UnusedAssignment") // canvas set to null to free memory
public static Bitmap createImageFromSeries(Context context, long seriesId) {
List<boolean[][][]> ballsOfGames = new ArrayList<>();
List<boolean[][]> foulsOfGames = new ArrayList<>();
List<Short> scoresOfGames = new ArrayList<>();
List<Boolean> manualScores = new ArrayList<>();
MainActivity.waitForSaveThreads(new WeakReference<>((MainActivity) context));
SQLiteDatabase database = DatabaseHelper.getInstance(context).getReadableDatabase();
String rawImageQuery = "SELECT "
+ GameEntry.COLUMN_GAME_NUMBER + ", "
+ GameEntry.COLUMN_SCORE + ", "
+ GameEntry.COLUMN_IS_MANUAL + ", "
+ FrameEntry.COLUMN_FRAME_NUMBER + ", "
+ FrameEntry.COLUMN_PIN_STATE[0] + ", "
+ FrameEntry.COLUMN_PIN_STATE[1] + ", "
+ FrameEntry.COLUMN_PIN_STATE[2] + ", "
+ FrameEntry.COLUMN_FOULS
+ " FROM " + GameEntry.TABLE_NAME + " AS game"
+ " INNER JOIN " + FrameEntry.TABLE_NAME
+ " ON game." + GameEntry._ID + "=" + FrameEntry.COLUMN_GAME_ID
+ " WHERE game." + GameEntry.COLUMN_SERIES_ID + "=?"
+ " ORDER BY " + GameEntry.COLUMN_GAME_NUMBER + ", "
+ FrameEntry.COLUMN_FRAME_NUMBER;
String[] rawImageArgs = new String[]{String.valueOf(seriesId)};
Cursor cursor = database.rawQuery(rawImageQuery, rawImageArgs);
int currentFrame = 0;
int currentGame = -1;
if (cursor.moveToFirst()) {
while (!cursor.isAfterLast()) {
int frameNumber = cursor.getInt(cursor.getColumnIndex(
FrameEntry.COLUMN_FRAME_NUMBER));
if (frameNumber == 1) {
currentFrame = 0;
currentGame++;
scoresOfGames.add(cursor.getShort(
cursor.getColumnIndex(GameEntry.COLUMN_SCORE)));
manualScores.add(cursor.getInt(
cursor.getColumnIndex(GameEntry.COLUMN_IS_MANUAL)) == 1);
ballsOfGames.add(new boolean[Constants.NUMBER_OF_FRAMES][3][5]);
foulsOfGames.add(new boolean[Constants.NUMBER_OF_FRAMES][3]);
}
for (int i = 0; i < 3; i++) {
int ball = cursor.getInt(cursor.getColumnIndex(FrameEntry.COLUMN_PIN_STATE[i]));
boolean[] ballBoolean = Score.ballIntToBoolean(ball);
ballsOfGames.get(currentGame)[currentFrame][i] = ballBoolean;
}
String fouls = Score.foulIntToString(
cursor.getInt(cursor.getColumnIndex(FrameEntry.COLUMN_FOULS)));
for (int ballCounter = 0; ballCounter < 3; ballCounter++) {
if (fouls.contains(String.valueOf(ballCounter + 1)))
foulsOfGames.get(currentGame)[currentFrame][ballCounter] = true;
}
currentFrame++;
cursor.moveToNext();
}
}
cursor.close();
cursor = null;
final int numberOfGames = ballsOfGames.size();
Paint paintText = new Paint();
paintText.setColor(Color.BLACK);
paintText.setTextSize(GAME_LARGE_FONT_SIZE);
Paint paintBlackOutline = new Paint();
paintBlackOutline.setColor(Color.BLACK);
Bitmap bitmap = Bitmap.createBitmap(BITMAP_SERIES_GAME_NAME_WIDTH + BITMAP_GAME_WIDTH,
(BITMAP_GAME_HEIGHT - 1) * numberOfGames + 1, Bitmap.Config.RGB_565);
Bitmap gameBitmap;
Canvas canvas = new Canvas(bitmap);
canvas.drawColor(Color.WHITE);
for (int i = 0; i < numberOfGames; i++) {
canvas.drawLine(0, BITMAP_GAME_HEIGHT * i - i, BITMAP_SERIES_GAME_NAME_WIDTH,
BITMAP_GAME_HEIGHT * i - i, paintBlackOutline);
canvas.drawText("Game " + (i + 1), 5,
BITMAP_GAME_HEIGHT * i + GAME_LARGE_FONT_SIZE / 2 + BITMAP_GAME_HEIGHT / 2 - i,
paintText);
gameBitmap = createImageFromGame(ballsOfGames.get(i), foulsOfGames.get(i),
scoresOfGames.get(i), manualScores.get(i));
canvas.drawBitmap(gameBitmap, BITMAP_SERIES_GAME_NAME_WIDTH, BITMAP_GAME_HEIGHT * i - i,
null);
gameBitmap.recycle();
System.gc();
}
canvas.drawLines(new float[]
{
0,
0,
BITMAP_SERIES_GAME_NAME_WIDTH,
0,
0,
0,
0,
(BITMAP_GAME_HEIGHT - 1) * numberOfGames,
0,
(BITMAP_GAME_HEIGHT - 1) * numberOfGames,
BITMAP_SERIES_GAME_NAME_WIDTH,
(BITMAP_GAME_HEIGHT - 1) * numberOfGames
}, paintBlackOutline);
canvas = null;
return bitmap;
}
/**
* A copy of the Android internals insertImage method, this method populates the meta data with DATE_ADDED and
* DATE_TAKEN. This fixes a common problem where media that is inserted manually gets saved at the end of the
* gallery (because date is not populated).
*
* @param cr n/a
* @param source n/a
* @param title n/a
* @param description n/a
* @return n/a
* @see android.provider.MediaStore.Images.Media#insertImage(android.content.ContentResolver, Bitmap, String,
* String)
*/
@SuppressWarnings("all")
// Ignoring try with automatic resource management in case java 1.6 is used
public static Uri insertImage(ContentResolver cr,
Bitmap source,
String title,
String description) {
ContentValues values = new ContentValues();
values.put(MediaStore.Images.Media.TITLE, title);
values.put(MediaStore.Images.Media.DISPLAY_NAME, title);
values.put(MediaStore.Images.Media.DESCRIPTION, description);
values.put(MediaStore.Images.Media.MIME_TYPE, "image/jpeg");
// Add the date meta data to ensure the image is added at the front of the gallery
values.put(MediaStore.Images.Media.DATE_ADDED, System.currentTimeMillis());
values.put(MediaStore.Images.Media.DATE_TAKEN, System.currentTimeMillis());
Uri url = null;
try {
url = cr.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, values);
if (source != null) {
OutputStream imageOut = cr.openOutputStream(url);
try {
source.compress(Bitmap.CompressFormat.JPEG, 50, imageOut);
} finally {
imageOut.close();
}
long id = ContentUris.parseId(url);
// Wait until MINI_KIND thumbnail is generated.
Bitmap miniThumb = MediaStore.Images.Thumbnails.getThumbnail(cr,
id,
MediaStore.Images.Thumbnails.MINI_KIND,
null);
// This is for backward compatibility.
storeThumbnail(cr,
miniThumb,
id,
50F,
50F,
MediaStore.Images.Thumbnails.MICRO_KIND);
} else {
cr.delete(url, null, null);
url = null;
}
} catch (Exception e) {
if (url != null) {
cr.delete(url, null, null);
url = null;
}
}
return url;
}
/**
* A copy of the Android internals StoreThumbnail method, it used with the insertImage to populate the
* android.provider.MediaStore.Images.Media#insertImage with all the correct meta data. The StoreThumbnail method is
* private so it must be duplicated here.
*
* @param cr n/a
* @param source n/a
* @param id n/a
* @param width n/a
* @param height n/a
* @param kind n/a
* @see android.provider.MediaStore.Images.Media (StoreThumbnail private method)
*/
private static void storeThumbnail(
ContentResolver cr,
Bitmap source,
long id,
float width,
float height,
int kind) {
// create the matrix to scale it
Matrix matrix = new Matrix();
float scaleX = width / source.getWidth();
float scaleY = height / source.getHeight();
matrix.setScale(scaleX, scaleY);
Bitmap thumb = Bitmap.createBitmap(source, 0, 0,
source.getWidth(),
source.getHeight(), matrix,
true
);
ContentValues values = new ContentValues(4);
values.put(MediaStore.Images.Thumbnails.KIND, kind);
values.put(MediaStore.Images.Thumbnails.IMAGE_ID, (int) id);
values.put(MediaStore.Images.Thumbnails.HEIGHT, thumb.getHeight());
values.put(MediaStore.Images.Thumbnails.WIDTH, thumb.getWidth());
Uri url = cr.insert(MediaStore.Images.Thumbnails.EXTERNAL_CONTENT_URI, values);
try {
if (url != null) {
OutputStream thumbOut = cr.openOutputStream(url);
thumb.compress(Bitmap.CompressFormat.JPEG, 100, thumbOut);
if (thumbOut != null)
thumbOut.close();
else
throw new IOException("Could not create output stream.");
}
} catch (IOException ex) {
Log.e(TAG, "Error saving thumbnail.", ex);
}
}
/**
* Default private constructor.
*/
private ImageUtils() {
// does nothing
}
}