Improve Player details, work in progress

feature/players
Aurelien Hubert 7 years ago
parent 75248ffdec
commit c2f79015bc
  1. 8
      app/build.gradle
  2. 4
      app/src/main/java/net/pokeranalytics/android/PokerAnalyticsApplication.kt
  3. 14
      app/src/main/java/net/pokeranalytics/android/model/migrations/PokerAnalyticsMigration.kt
  4. 10
      app/src/main/java/net/pokeranalytics/android/model/realm/Player.kt
  5. 1
      app/src/main/java/net/pokeranalytics/android/ui/activity/EditableDataActivity.kt
  6. 215
      app/src/main/java/net/pokeranalytics/android/ui/activity/components/MediaActivity.kt
  7. 138
      app/src/main/java/net/pokeranalytics/android/ui/fragment/data/PlayerDataFragment.kt
  8. 52
      app/src/main/java/net/pokeranalytics/android/ui/view/RowViewType.kt
  9. 62
      app/src/main/java/net/pokeranalytics/android/ui/view/rowrepresentable/PlayerRow.kt
  10. 377
      app/src/main/java/net/pokeranalytics/android/util/ImageUtils.kt
  11. 12
      app/src/main/res/drawable/circle_stroke_kaki.xml
  12. 38
      app/src/main/res/layout/fragment_player.xml
  13. 46
      app/src/main/res/layout/row_player_image.xml
  14. 11
      app/src/main/res/values/styles.xml

@ -48,7 +48,7 @@ android {
def appName = "PokerAnalytics"
def buildType = variant.variantData.variantConfiguration.buildType.name
def newName
if (buildType == 'debug'){
if (buildType == 'debug') {
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_debug.apk"
} else {
newName = "${appName}_${defaultConfig.versionName}(${defaultConfig.versionCode})_${formattedDate}_release.apk"
@ -71,7 +71,7 @@ dependencies {
implementation fileTree(dir: 'libs', include: ['*.jar'])
// Kotlin
implementation"org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.1.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:1.1.1"
implementation "org.jetbrains.kotlin:kotlin-reflect:$kotlin_version"
@ -90,6 +90,10 @@ dependencies {
implementation 'com.squareup.okhttp3:logging-interceptor:3.9.1'
implementation 'com.google.code.gson:gson:2.8.5'
// Glide
implementation 'com.github.bumptech.glide:glide:4.9.0'
annotationProcessor 'com.github.bumptech.glide:compiler:4.9.0'
// Places
implementation 'com.google.android.libraries.places:places:1.1.0'

@ -33,7 +33,7 @@ class PokerAnalyticsApplication : Application() {
Realm.init(this)
val realmConfiguration = RealmConfiguration.Builder()
.name(Realm.DEFAULT_REALM_NAME)
.schemaVersion(6)
.schemaVersion(7)
.migration(PokerAnalyticsMigration())
.initialData(Seed(this))
.build()
@ -60,7 +60,7 @@ class PokerAnalyticsApplication : Application() {
if (BuildConfig.DEBUG) {
Timber.d("UserPreferences.defaultCurrency: ${UserDefaults.currency.symbol}")
// this.createFakeSessions()
this.createFakeSessions()
}
Patcher.patchAll(this.applicationContext)

@ -142,8 +142,22 @@ class PokerAnalyticsMigration : RealmMigration {
schema.get("Filter")?.addField("filterableTypeUniqueIdentifier", Integer::class.java)
schema.get("Filter")?.addField("useCount", Int::class.java)
schema.get("Filter")?.removeField("usageCount")
currentVersion++
}
// Migrate to version 7
Timber.d("currentVersion: $currentVersion")
if (currentVersion == 6) {
schema.get("Player")?.let {
it.addField("summary", String::class.java).setRequired("summary", true)
it.addField("color", Int::class.java).setNullable("color", true)
it.addField("picture", String::class.java)
}
}
}
override fun equals(other: Any?): Boolean {

@ -10,6 +10,7 @@ import net.pokeranalytics.android.model.interfaces.NameManageable
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
@ -19,7 +20,9 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent
companion object {
val rowRepresentation: List<RowRepresentable> by lazy {
val rows = ArrayList<RowRepresentable>()
rows.add(PlayerRow.PLAYER_IMAGE)
rows.add(SimpleRow.NAME)
rows.add(PlayerRow.SUMMARY)
rows
}
}
@ -31,6 +34,12 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent
override var name: String = ""
// New fields
var summary: String = ""
var color: Int? = null
var picture: String? = null
override fun isValidForDelete(realm: Realm): Boolean {
TODO("not implemented") //To change body of created functions use File | Settings | File Templates.
}
@ -64,6 +73,7 @@ open class Player : RealmObject(), NameManageable, Deletable, StaticRowRepresent
override fun updateValue(value: Any?, row: RowRepresentable) {
when (row) {
SimpleRow.NAME -> this.name = value as String? ?: ""
PlayerRow.SUMMARY -> this.summary = value as String? ?: ""
}
}

@ -65,6 +65,7 @@ class EditableDataActivity : PokerAnalyticsActivity() {
LiveData.TRANSACTION.ordinal -> TransactionDataFragment()
LiveData.CUSTOM_FIELD.ordinal -> CustomFieldDataFragment()
LiveData.TRANSACTION_TYPE.ordinal -> TransactionTypeDataFragment()
LiveData.PLAYER.ordinal -> PlayerDataFragment()
else -> EditableDataFragment()
}

@ -0,0 +1,215 @@
package net.pokeranalytics.android.ui.activity.components
import android.Manifest
import android.app.Activity
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Bitmap
import android.net.Uri
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.util.ImageUtils
import timber.log.Timber
import java.io.File
import java.util.*
open class MediaActivity : PokerAnalyticsActivity() {
companion object {
const val SELECTED_CHOICE_TAKE_PICTURE = 10
const val SELECTED_CHOICE_SELECT_PICTURE = 11
const val REQUEST_CODE_TAKE_PICTURE = 100
const val REQUEST_CODE_SELECT_PICTURE = 101
const val PERMISSION_REQUEST_EXTERNAL_STORAGE = 201
const val PERMISSION_REQUEST_CAMERA = 202
}
// Data
private val outputFileUri: Uri? = null
private val maxSampleWidth = 1024
private val maxSampleHeight = 1024
private val resizeImage = true
private var tempFile: File? = null
private var mCurrentPhotoPath: String? = null
private var selectedChoice = -1
private var multiplePictures = false
override fun onDestroy() {
super.onDestroy()
if (tempFile != null) {
tempFile!!.delete()
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (resultCode == Activity.RESULT_OK) {
if (requestCode == REQUEST_CODE_SELECT_PICTURE || requestCode == REQUEST_CODE_TAKE_PICTURE) {
val filesList = ArrayList<File>()
GlobalScope.launch {
if (data?.clipData != null) {
data?.clipData?.let { clipData ->
try {
GlobalScope.launch(Dispatchers.Main) {
isLoadingNewPhotos()
}
for (i in 0 until clipData.itemCount) {
val item = clipData.getItemAt(i)
val uri = item.uri
val inputStream = contentResolver.openInputStream(uri)
val photoFile = ImageUtils.createTempImageFile(this@MediaActivity)
ImageUtils.copyInputStreamToFile(inputStream!!, photoFile)
filesList.add(photoFile)
}
GlobalScope.launch(Dispatchers.Main) {
getPhotos(filesList)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
} else if (data?.data != null) {
data?.data?.let { uri ->
try {
GlobalScope.launch(Dispatchers.Main) {
isLoadingNewPhotos()
}
val inputStream = contentResolver.openInputStream(uri)
val photoFile = ImageUtils.createTempImageFile(this@MediaActivity)
ImageUtils.copyInputStreamToFile(inputStream!!, photoFile)
filesList.add(photoFile)
GlobalScope.launch(Dispatchers.Main) {
getPhotos(filesList)
}
} catch (e: Exception) {
e.printStackTrace()
}
}
}
}
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (grantResults.isNotEmpty()) {
for (result in grantResults) {
if (result != PackageManager.PERMISSION_GRANTED) {
//Toast.makeText(this, getString(R.string.photo_library_add_usage_description), Toast.LENGTH_SHORT).show()
selectedChoice = -1
return
}
}
when (selectedChoice) {
SELECTED_CHOICE_SELECT_PICTURE -> {
Timber.d("openImageGalleryIntent")
openImageGalleryIntent(multiplePictures)
}
}
}
selectedChoice = -1
}
/**
* Open the gallery intent
*/
fun openImageGalleryIntent(multiplePictures: Boolean) {
this.multiplePictures = multiplePictures
// Test if we have the permission
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
selectedChoice = SELECTED_CHOICE_SELECT_PICTURE
askForStoragePermission()
return
}
this.multiplePictures = multiplePictures
val galleryIntent = Intent()
galleryIntent.type = "image/*"
galleryIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, multiplePictures)
galleryIntent.action = Intent.ACTION_GET_CONTENT
startActivityForResult(galleryIntent, REQUEST_CODE_SELECT_PICTURE)
}
/**
* Ask for the external storage permission
*/
private fun askForStoragePermission() {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE),
PERMISSION_REQUEST_EXTERNAL_STORAGE)
}
}
/**
* Ask for the acmera permission
*/
private fun askForCameraPermission() {
// Here, thisActivity is the current activity
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this, arrayOf(Manifest.permission.CAMERA),
PERMISSION_REQUEST_CAMERA)
}
}
/**
* Ask for camera and storage permission
*/
private fun askForCameraAndStoragePermissions() {
val permissions = ArrayList<String>()
if (ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.CAMERA)
}
if (ContextCompat.checkSelfPermission(this, Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) {
permissions.add(Manifest.permission.WRITE_EXTERNAL_STORAGE)
}
if (permissions.size > 0) {
ActivityCompat.requestPermissions(this, permissions.toArray(arrayOfNulls<String>(permissions.size)), PERMISSION_REQUEST_CAMERA)
}
}
/**
* Called when a bitmap is return
*
* @param bitmap the bitmap returned
*/
open fun getBitmapImage(file: File?, bitmap: Bitmap?) {}
/**
* Called when the user is adding new photos
*/
open fun isLoadingNewPhotos() {}
/**
* Called when the user has selected photos
*/
open fun getPhotos(files: ArrayList<File>) {}
}

@ -0,0 +1,138 @@
package net.pokeranalytics.android.ui.fragment.data
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import net.pokeranalytics.android.R
import net.pokeranalytics.android.model.realm.Player
import net.pokeranalytics.android.ui.adapter.RowRepresentableDataSource
import net.pokeranalytics.android.ui.adapter.StaticRowRepresentableDataSource
import net.pokeranalytics.android.ui.extensions.toast
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetFragment
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.rowrepresentable.PlayerRow
import net.pokeranalytics.android.ui.view.rowrepresentable.SimpleRow
import net.pokeranalytics.android.util.NULL_TEXT
import java.util.*
/**
* Player data fragment
*/
class PlayerDataFragment : EditableDataFragment(), StaticRowRepresentableDataSource {
private val player: Player
get() {
return this.item as Player
}
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?): View? {
super.onCreateView(inflater, container, savedInstanceState)
return inflater.inflate(R.layout.fragment_player, container, false)
}
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
initUI()
}
override fun getDataSource(): RowRepresentableDataSource {
return this
}
override fun adapterRows(): List<RowRepresentable>? {
return player.adapterRows()
}
override fun stringForRow(row: RowRepresentable): String {
return when (row) {
PlayerRow.PLAYER_IMAGE -> {
if (player.name.isNotEmpty()) {
val playerData = player.name.split(" ")
if (playerData.size > 1) {
playerData[0].first().toString() + playerData[1].first().toString()
} else if (player.name.length > 1) {
player.name.substring(0, 2)
} else {
player.name.substring(0, player.name.length)
}
} else {
NULL_TEXT
}
}
SimpleRow.NAME -> if (player.name.isNotEmpty()) player.name else NULL_TEXT
PlayerRow.SUMMARY -> if (player.summary.isNotEmpty()) player.summary else NULL_TEXT
else -> super.stringForRow(row)
}
}
override fun editDescriptors(row: RowRepresentable): ArrayList<RowRepresentableEditDescriptor>? {
return when (row) {
SimpleRow.NAME -> row.editingDescriptors(mapOf("defaultValue" to this.player.name))
PlayerRow.SUMMARY -> row.editingDescriptors(mapOf("defaultValue" to this.player.summary))
else -> null
}
}
override fun onRowSelected(position: Int, row: RowRepresentable, fromAction: Boolean) {
when(row) {
PlayerRow.PLAYER_IMAGE -> {
toast("Yo.")
}
PlayerRow.SUMMARY -> {
val data = editDescriptors(row)
BottomSheetFragment.create(fragmentManager, row, this, data, isClearable = false, isDeletable = true)
}
SimpleRow.NAME -> super.onRowSelected(position, row, fromAction)
}
}
override fun onRowValueChanged(value: Any?, row: RowRepresentable) {
super.onRowValueChanged(value, row)
when(row) {
SimpleRow.NAME -> rowRepresentableAdapter.refreshRow(PlayerRow.PLAYER_IMAGE)
}
}
override fun onRowDeleted(row: RowRepresentable) {
super.onRowDeleted(row)
}
/**
* Init UI
*/
private fun initUI() {
/*
customField.updateRowRepresentation()
bottomBar.translationY = 72f.px
bottomBar.visibility = View.VISIBLE
*/
/*
addItem.setOnClickListener {
val customFieldEntry = player.addEntry()
rowRepresentableAdapter.notifyDataSetChanged()
onRowSelected(-1, customFieldEntry)
}
*/
/*
updateUI()
rowRepresentableAdapter.notifyDataSetChanged()
if (!this.deleteButtonShouldAppear) {
rowRepresentableForPosition(0)?.let {
onRowSelected(0, it)
}
}
*/
}
}

@ -78,6 +78,7 @@ enum class RowViewType(private var layoutRes: Int) {
// Custom row
ROW_SESSION(R.layout.row_feed_session),
ROW_TRANSACTION(R.layout.row_transaction),
ROW_PLAYER_IMAGE(R.layout.row_player_image),
ROW_BUTTON(R.layout.row_button),
ROW_FOLLOW_US(R.layout.row_follow_us),
STATS(R.layout.row_stats_title_value),
@ -86,6 +87,7 @@ enum class RowViewType(private var layoutRes: Int) {
LEGEND_DEFAULT(R.layout.row_legend_default),
LIST(R.layout.row_list),
// Separator
SEPARATOR(R.layout.row_separator);
@ -110,6 +112,8 @@ enum class RowViewType(private var layoutRes: Int) {
// Row Transaction
ROW_TRANSACTION -> RowTransactionViewHolder(layout)
ROW_PLAYER_IMAGE -> RowPlayerImageViewHolder(layout)
// Row Button
ROW_BUTTON -> RowButtonViewHolder(layout)
@ -401,17 +405,17 @@ enum class RowViewType(private var layoutRes: Int) {
override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) {
if (row is BankrollReport) {
itemView.findViewById<AppCompatTextView>(R.id.stat1Name)?.let {
itemView.findViewById<AppCompatTextView?>(R.id.stat1Name)?.let {
it.text = itemView.context.getString(R.string.total)
}
itemView.findViewById<AppCompatTextView>(R.id.stat1Value)?.let {
itemView.findViewById<AppCompatTextView?>(R.id.stat1Value)?.let {
val formattedStat = ComputedStat(Stat.NET_RESULT, row.total).format()
it.setTextFormat(formattedStat, itemView.context)
}
itemView.findViewById<AppCompatTextView>(R.id.stat2Name)?.let {
itemView.findViewById<AppCompatTextView?>(R.id.stat2Name)?.let {
it.text = itemView.context.getString(R.string.risk_of_ruin)
}
itemView.findViewById<AppCompatTextView>(R.id.stat2Value)?.let {
itemView.findViewById<AppCompatTextView?>(R.id.stat2Value)?.let {
val riskOfRuin = row.riskOfRuin ?: 0.0
val formattedStat = ComputedStat(Stat.RISK_OF_RUIN, riskOfRuin).format()
it.setTextFormat(formattedStat, itemView.context)
@ -419,7 +423,7 @@ enum class RowViewType(private var layoutRes: Int) {
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
itemView.findViewById<ConstraintLayout>(R.id.container)?.setOnClickListener(listener)
itemView.findViewById<ConstraintLayout?>(R.id.container)?.setOnClickListener(listener)
}
}
@ -434,7 +438,7 @@ enum class RowViewType(private var layoutRes: Int) {
if (row is CustomFieldRow) {
itemView.findViewById<ChipGroup>(R.id.chipGroup)?.let { chipGroup ->
itemView.findViewById<ChipGroup?>(R.id.chipGroup)?.let { chipGroup ->
chipGroup.removeAllViews()
chipGroup.setOnCheckedChangeListener(null)
@ -477,9 +481,9 @@ enum class RowViewType(private var layoutRes: Int) {
*/
inner class RowButtonViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder {
override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) {
itemView.findViewById<AppCompatTextView>(R.id.title).text = row.localizedTitle(itemView.context)
itemView.findViewById<AppCompatTextView>(R.id.title).isVisible = !adapter.dataSource.boolForRow(row)
itemView.findViewById<ContentLoadingProgressBar>(R.id.progressBar).isVisible = adapter.dataSource.boolForRow(row)
itemView.findViewById<AppCompatTextView?>(R.id.title)?.text = row.localizedTitle(itemView.context)
itemView.findViewById<AppCompatTextView?>(R.id.title)?.isVisible = !adapter.dataSource.boolForRow(row)
itemView.findViewById<ContentLoadingProgressBar?>(R.id.progressBar)?.isVisible = adapter.dataSource.boolForRow(row)
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
@ -513,6 +517,36 @@ enum class RowViewType(private var layoutRes: Int) {
}
}
/**
* Display a transaction view
*/
inner class RowPlayerImageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView), BindableHolder {
override fun bind(position: Int, row: RowRepresentable, adapter: RowRepresentableAdapter) {
itemView.findViewById<AppCompatTextView?>(R.id.playerInitial)?.let { textView ->
textView.text = adapter.dataSource.stringForRow(row)
}
itemView.findViewById<AppCompatImageView?>(R.id.playerImage)?.let { imageView ->
}
itemView.findViewById<AppCompatImageView?>(R.id.playerImageSelection)?.let { imageView ->
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
imageView.setOnClickListener(listener)
}
/*
itemView.transactionRow.setData(row as Transaction)
val listener = View.OnClickListener {
adapter.delegate?.onRowSelected(position, row)
}
itemView.transactionRow.setOnClickListener(listener)
*/
}
}
/**
* Display a separator
*/

@ -0,0 +1,62 @@
package net.pokeranalytics.android.ui.view.rowrepresentable
import net.pokeranalytics.android.R
import net.pokeranalytics.android.ui.fragment.components.bottomsheet.BottomSheetType
import net.pokeranalytics.android.ui.view.RowRepresentable
import net.pokeranalytics.android.ui.view.RowRepresentableEditDescriptor
import net.pokeranalytics.android.ui.view.RowViewType
/**
* An enum managing the player rows
*/
enum class PlayerRow : RowRepresentable {
PLAYER_IMAGE,
SUMMARY;
companion object {
/**
* Return the report rows
*/
fun getRows(): ArrayList<RowRepresentable> {
val rows = ArrayList<RowRepresentable>()
rows.addAll(values())
return rows
}
}
override val resId: Int?
get() {
return when (this) {
PLAYER_IMAGE -> R.string.app_name
SUMMARY -> R.string.summary
}
}
override val viewType: Int
get() {
return when (this) {
PLAYER_IMAGE -> RowViewType.ROW_PLAYER_IMAGE.ordinal
SUMMARY -> RowViewType.TITLE_VALUE.ordinal
}
}
override fun editingDescriptors(map: Map<String, Any?>): ArrayList<RowRepresentableEditDescriptor>? {
return when (this) {
SUMMARY -> {
val defaultValue: String? by map
arrayListOf(RowRepresentableEditDescriptor(defaultValue, R.string.summary))
}
else -> super.editingDescriptors(map)
}
}
override val bottomSheetType: BottomSheetType
get() {
return when (this) {
SUMMARY -> BottomSheetType.EDIT_TEXT_MULTI_LINES
else -> BottomSheetType.NONE
}
}
}

@ -0,0 +1,377 @@
package net.pokeranalytics.android.util
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.graphics.*
import android.graphics.Paint.FILTER_BITMAP_FLAG
import android.media.ExifInterface
import android.net.Uri
import android.os.Environment
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import net.pokeranalytics.android.R
import timber.log.Timber
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.io.InputStream
import java.text.SimpleDateFormat
import java.util.*
object ImageUtils {
/**
* Rotate a bitmap if it's necessary (depending of the EXIF data)
* Some devices don't rotate the picture but instead add the orientation
* value in the EXIF data.
* That's why we need sometimes to rotate by ourselves the bitmap
*
* @param src The file to check (for getting the Exif data)
* @param bitmap The bitmap to modify (if necessary)
* @return The bitmap in the correct orientation
*/
fun rotateBitmap(src: String, bitmap: Bitmap, updateFile: Boolean): Bitmap {
try {
val orientation = getExifOrientation(src)
if (orientation == ExifInterface.ORIENTATION_NORMAL) {
return bitmap
}
val matrix = Matrix()
when (orientation) {
ExifInterface.ORIENTATION_FLIP_HORIZONTAL -> matrix.setScale(-1f, 1f)
ExifInterface.ORIENTATION_ROTATE_180 -> matrix.setRotate(180f)
ExifInterface.ORIENTATION_FLIP_VERTICAL -> {
matrix.setRotate(180f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_TRANSPOSE -> {
matrix.setRotate(90f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_ROTATE_90 -> matrix.setRotate(90f)
ExifInterface.ORIENTATION_TRANSVERSE -> {
matrix.setRotate(-90f)
matrix.postScale(-1f, 1f)
}
ExifInterface.ORIENTATION_ROTATE_270 -> matrix.setRotate(-90f)
else -> return bitmap
}
try {
val oriented = Bitmap.createBitmap(bitmap, 0, 0, bitmap.width, bitmap.height, matrix, true)
bitmap.recycle()
if (updateFile) {
updateFile(src, oriented)
}
return oriented
} catch (e: OutOfMemoryError) {
e.printStackTrace()
return bitmap
}
} catch (e: IOException) {
e.printStackTrace()
}
return bitmap
}
/**
* Get the Exif orientation value
*
* @param filePath The path of the file
* @return the orientation value
* @throws IOException
*/
@Throws(IOException::class)
private fun getExifOrientation(filePath: String): Int {
val exifInterface = ExifInterface(filePath)
return exifInterface.getAttributeInt(ExifInterface.TAG_ORIENTATION, ExifInterface.ORIENTATION_NORMAL)
}
/**
* Save a bitmap into a file (& apply 90% compression)
*
* @param filePath Path of the file
* @param bitmap Bitmap to save
*/
fun updateFile(filePath: String, bitmap: Bitmap) {
var out: FileOutputStream? = null
try {
out = FileOutputStream(filePath)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
out?.close()
} catch (e: IOException) {
e.printStackTrace()
}
}
}
/**
* Resize a file with the given maximum width or height (and keep the ratio!)
* @param filePath String: File path
* @param bitmap Bitmap: Image
* @param maxWidth int: Max width
* @param maxHeight int: Max height
*/
fun resizeFile(filePath: String, bitmap: Bitmap, maxWidth: Int, maxHeight: Int) {
var bitmap = bitmap
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
val imageWidth = options.outWidth
val imageHeight = options.outHeight
var newWidth: Int
var newHeight: Int
if (imageWidth > imageHeight) {
newWidth = maxWidth
newHeight = imageHeight * maxWidth / imageWidth
if (newHeight > maxHeight) {
newHeight = maxHeight
newWidth = imageWidth * maxHeight / imageHeight
}
} else {
newHeight = maxHeight
newWidth = imageWidth * maxHeight / imageHeight
if (newWidth > maxWidth) {
newWidth = maxWidth
newHeight = imageHeight * maxWidth / imageWidth
}
}
bitmap = Bitmap.createScaledBitmap(bitmap, newWidth, newHeight, true)
updateFile(filePath, bitmap)
}
/**
* Create a unique temp image file name
*
* @return
* @throws IOException
*/
@Throws(IOException::class)
fun createTempImageFile(context: Context): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storageDir = context.cacheDir
return File.createTempFile(imageFileName, ".jpg", storageDir)
}
/**
* Create a unique image file name
*
* @return
* @throws IOException
*/
@Throws(IOException::class)
fun createImageFile(context: Context): File {
// Create an image file name
val timeStamp = SimpleDateFormat("yyyyMMdd_HHmmss", Locale.getDefault()).format(Date())
val imageFileName = "JPEG_" + timeStamp + "_"
val storage = ContextCompat.getExternalFilesDirs(context, Environment.DIRECTORY_PICTURES)
val storageDir = if (storage.isNotEmpty()) storage.first() else Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_PICTURES)
val appStorageDir = File(storageDir.path + "/" + context.getString(R.string.app_name))
if (!appStorageDir.exists()) {
appStorageDir.mkdirs()
}
return File.createTempFile(imageFileName, ".jpg", appStorageDir)
}
/**
* Decode sample Bitmap
*
* @param filePath The bitmap file path
* @param reqWidth Max width required
* @param reqHeight Max height required
* @return The sampled bitmap
*/
fun decodeSampledBitmapFromFile(filePath: String, reqWidth: Int, reqHeight: Int): Bitmap {
// First decode with inJustDecodeBounds=true to check dimensions
val options = BitmapFactory.Options()
options.inJustDecodeBounds = true
BitmapFactory.decodeFile(filePath, options)
// Calculate inSampleSize
options.inSampleSize = calculateInSampleSize(options, reqWidth, reqHeight)
// Decode bitmap with inSampleSize set
options.inJustDecodeBounds = false
return BitmapFactory.decodeFile(filePath, options)
}
/**
* Calculate the sample size
*/
fun calculateInSampleSize(options: BitmapFactory.Options, reqWidth: Int,
reqHeight: Int): Int {
// Raw height and width of image
val height = options.outHeight
val width = options.outWidth
var inSampleSize = 1
if (height > reqHeight || width > reqWidth) {
val halfHeight = height / 2
val halfWidth = width / 2
// Calculate the largest inSampleSize value that is a power of 2 and keeps both
// height and width larger than the requested height and width.
while (halfHeight / inSampleSize > reqHeight && halfWidth / inSampleSize > reqWidth) {
inSampleSize *= 2
}
}
return inSampleSize
}
/**
* Copy an input stream inside a file
*
* @param in Input Stream
* @param file Destination file
*/
fun copyInputStreamToFile(inputStream: InputStream, file: File) {
try {
val out = FileOutputStream(file)
val buf = ByteArray(4096)
var len = 0
while ({ len = inputStream.read(buf); len }() > 0) {
out.write(buf, 0, len)
}
out.close()
inputStream.close()
} catch (e: Exception) {
e.printStackTrace()
}
}
/**
* Update the gallery with the current file
*
* @param context Context
* @param filePath The file to add
*/
fun updateGallery(context: Context, filePath: String) {
val mediaScanIntent = Intent(Intent.ACTION_MEDIA_SCANNER_SCAN_FILE)
val f = File(filePath)
val contentUri = Uri.fromFile(f)
mediaScanIntent.data = contentUri
context.sendBroadcast(mediaScanIntent)
}
/**
* Save the bitmap in a file
*/
fun saveBitmapInFile(context: Context, bitmap: Bitmap, filename: String, action: (filePath: String) -> Unit) {
GlobalScope.launch {
val outputFile = File(context.filesDir, filename)
var out: FileOutputStream? = null
try {
out = FileOutputStream(outputFile.absolutePath)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out) // bmp is your Bitmap instance
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
if (out != null) {
out.close()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
GlobalScope.launch(Dispatchers.Main) {
Timber.d("Save file here: ${outputFile.absolutePath}")
action(outputFile.absolutePath)
}
}
}
/**
* Bitmap resizer
*/
fun bitmapResizer(bitmap: Bitmap, newWidth: Int, newHeight: Int): Bitmap {
val scaledBitmap = Bitmap.createBitmap(newWidth, newHeight, Bitmap.Config.ARGB_8888)
val ratioX = newWidth / bitmap.width.toFloat()
val ratioY = newHeight / bitmap.height.toFloat()
val middleX = newWidth / 2.0f
val middleY = newHeight / 2.0f
val scaleMatrix = Matrix()
scaleMatrix.setScale(ratioX, ratioY, middleX, middleY)
val canvas = Canvas(scaledBitmap)
canvas.matrix = scaleMatrix
canvas.drawBitmap(bitmap, middleX - bitmap.width / 2, middleY - bitmap.height / 2, Paint(FILTER_BITMAP_FLAG))
return scaledBitmap
}
/**
* Export a bitmap
*/
private fun exportFile(context: Activity, bitmap: Bitmap) {
/*
val outputFile = File.createTempFile("test_export", ".jpg", context.cacheDir)
var out: FileOutputStream? = null
try {
out = FileOutputStream(outputFile.absolutePath)
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, out)
} catch (e: Exception) {
e.printStackTrace()
} finally {
try {
if (out != null) {
out.close()
}
} catch (e: IOException) {
e.printStackTrace()
}
}
val uri = FileProvider.getUriForFile(context,
context.packageName + ".provider", outputFile)
val shareIntent = ShareCompat.IntentBuilder.from(context)
.setType("image/jpg")
.setSubject(context.getString(R.string.share_file_name))
.setStream(uri)
.setChooserTitle(context.getString(R.string.share_title))
.createChooserIntent()
.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(shareIntent)
*/
}
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
android:shape="oval">
<stroke android:color="@color/kaki"
android:width="1dp"/>
<size
android:width="96dp"
android:height="96dp" />
</shape>

@ -0,0 +1,38 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:animateLayoutChanges="true">
<com.google.android.material.appbar.AppBarLayout
android:id="@+id/appBar"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:theme="@style/PokerAnalyticsTheme.Toolbar.Session"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent">
<androidx.appcompat.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_height="?attr/actionBarSize"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:title="@string/stats" />
</com.google.android.material.appbar.AppBarLayout>
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerView"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/appBar" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -0,0 +1,46 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/gray_dark"
android:foreground="?selectableItemBackground">
<androidx.appcompat.widget.AppCompatTextView
android:id="@+id/playerInitial"
style="@style/PokerAnalyticsTheme.TextView.Player"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_margin="16dp"
android:background="?selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="AH" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/playerImage"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_margin="16dp"
android:background="?selectableItemBackgroundBorderless"
android:foreground="@drawable/circle_stroke_kaki"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<androidx.appcompat.widget.AppCompatImageView
android:id="@+id/playerImageSelection"
android:layout_width="70dp"
android:layout_height="70dp"
android:layout_margin="16dp"
android:background="?selectableItemBackgroundBorderless"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>

@ -240,6 +240,17 @@
<item name="android:textSize">16sp</item>
</style>
<!-- Player Row -->
<style name="PokerAnalyticsTheme.TextView.Player">
<item name="android:textColor">@color/kaki</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
<item name="android:textAllCaps">true</item>
<item name="android:fontFamily">@font/roboto</item>
<item name="android:textSize">32sp</item>
<item name="android:gravity">center</item>
</style>
<!-- Alert Dialog -->

Loading…
Cancel
Save