Compare commits
No commits in common. 'master' and 'currency' have entirely different histories.
@ -1,139 +0,0 @@ |
|||||||
# CLAUDE.md |
|
||||||
|
|
||||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. |
|
||||||
|
|
||||||
## Project Overview |
|
||||||
|
|
||||||
This is **Poker Analytics**, a comprehensive Android application for tracking and analyzing poker sessions. The app supports both cash games and tournaments, providing detailed statistics, reporting, and data visualization capabilities. |
|
||||||
|
|
||||||
### Key Features |
|
||||||
- Session tracking (cash games and tournaments) |
|
||||||
- Advanced filtering and reporting |
|
||||||
- Bankroll management |
|
||||||
- Hand history import and replay |
|
||||||
- Statistical analysis and charting |
|
||||||
- Data backup and export functionality |
|
||||||
- Multi-currency support |
|
||||||
|
|
||||||
## Development Commands |
|
||||||
|
|
||||||
### Building the Project |
|
||||||
```bash |
|
||||||
./gradlew assembleStandardRelease # Build release APK |
|
||||||
./gradlew assembleStandardDebug # Build debug APK |
|
||||||
./gradlew build # Build all variants |
|
||||||
``` |
|
||||||
|
|
||||||
### Running Tests |
|
||||||
```bash |
|
||||||
./gradlew test # Run unit tests |
|
||||||
./gradlew connectedAndroidTest # Run instrumented tests (requires device/emulator) |
|
||||||
./gradlew testStandardDebugUnitTest # Run specific unit tests |
|
||||||
``` |
|
||||||
|
|
||||||
### Cleaning |
|
||||||
```bash |
|
||||||
./gradlew clean # Clean build artifacts |
|
||||||
``` |
|
||||||
|
|
||||||
### Build Configuration |
|
||||||
- **Target SDK**: 35 (Android 15) |
|
||||||
- **Min SDK**: 23 (Android 6.0) |
|
||||||
- **Build Tools**: 30.0.3 |
|
||||||
- **Kotlin Version**: 1.9.24 |
|
||||||
- **Realm Schema Version**: 14 |
|
||||||
|
|
||||||
## Architecture Overview |
|
||||||
|
|
||||||
### Package Structure |
|
||||||
The main source code is organized under `app/src/main/java/net/pokeranalytics/android/`: |
|
||||||
|
|
||||||
#### Core Components |
|
||||||
- **`model/`** - Data models and business logic |
|
||||||
- `realm/` - Realm database models (Session, Bankroll, Result, etc.) |
|
||||||
- `filter/` - Query system for filtering sessions |
|
||||||
- `migrations/` - Database migration handling |
|
||||||
- `handhistory/` - Hand history data structures |
|
||||||
|
|
||||||
- **`ui/`** - User interface components |
|
||||||
- `activity/` - Main activities (HomeActivity, SessionActivity, etc.) |
|
||||||
- `fragment/` - UI fragments organized by feature |
|
||||||
- `adapter/` - RecyclerView adapters and data sources |
|
||||||
- `modules/` - Feature-specific UI modules |
|
||||||
|
|
||||||
- **`calculus/`** - Statistics and calculation engine |
|
||||||
- Core calculation logic for poker statistics |
|
||||||
- Report generation system |
|
||||||
- Performance tracking |
|
||||||
|
|
||||||
- **`util/`** - Utility classes |
|
||||||
- `csv/` - CSV import/export functionality |
|
||||||
- `billing/` - In-app purchase handling |
|
||||||
- `extensions/` - Kotlin extension functions |
|
||||||
|
|
||||||
#### Key Classes |
|
||||||
- **`Session`** (`model/realm/Session.kt`): Core session data model |
|
||||||
- **`HomeActivity`** (`ui/activity/HomeActivity.kt`): Main app entry point with tab navigation |
|
||||||
- **`PokerAnalyticsApplication`**: Application class handling initialization |
|
||||||
|
|
||||||
### Database Architecture |
|
||||||
The app uses **Realm** database with these key entities: |
|
||||||
- `Session` - Individual poker sessions |
|
||||||
- `Bankroll` - Bankroll management |
|
||||||
- `Result` - Session results and statistics |
|
||||||
- `ComputableResult` - Pre-computed statistics for performance |
|
||||||
- `Filter` - Saved filter configurations |
|
||||||
- `HandHistory` - Hand-by-hand game data |
|
||||||
|
|
||||||
### UI Architecture |
|
||||||
- **MVVM pattern** with Android Architecture Components |
|
||||||
- **Fragment-based navigation** with bottom navigation tabs |
|
||||||
- **Custom RecyclerView adapters** for data presentation |
|
||||||
- **Material Design** components |
|
||||||
|
|
||||||
## Key Technologies |
|
||||||
|
|
||||||
### Core Dependencies |
|
||||||
- **Realm Database** (10.15.1) - Local data storage |
|
||||||
- **Kotlin Coroutines** - Asynchronous programming |
|
||||||
- **Firebase Crashlytics** - Crash reporting and analytics |
|
||||||
- **Material Design Components** - UI framework |
|
||||||
- **MPAndroidChart** - Data visualization |
|
||||||
- **CameraX** - Image capture functionality |
|
||||||
|
|
||||||
### Testing |
|
||||||
- **JUnit** for unit testing |
|
||||||
- **Android Instrumented Tests** for integration testing |
|
||||||
- Test files located in `app/src/androidTest/` and `app/src/test/` |
|
||||||
|
|
||||||
## Development Guidelines |
|
||||||
|
|
||||||
### Working with Sessions |
|
||||||
- Sessions are the core data model representing individual poker games |
|
||||||
- Use `Session.newInstance()` to create new sessions properly |
|
||||||
- Always call `computeStats()` after modifying session data |
|
||||||
- Sessions can be in various states: PENDING, STARTED, PAUSED, ENDED |
|
||||||
|
|
||||||
### Database Operations |
|
||||||
- Use Realm transactions for data modifications |
|
||||||
- The app uses schema version 14 - increment when making schema changes |
|
||||||
- Migration logic is in `PokerAnalyticsMigration.kt` |
|
||||||
|
|
||||||
### Testing Data |
|
||||||
- Use `FakeDataManager.createFakeSessions()` for generating test data |
|
||||||
- Seed data is available through the `Seed` class |
|
||||||
|
|
||||||
### Build Variants |
|
||||||
- **standard** - Main production flavor |
|
||||||
- Release builds are optimized and obfuscated with ProGuard |
|
||||||
|
|
||||||
## Performance Considerations |
|
||||||
- Sessions use `ComputableResult` for pre-computed statistics |
|
||||||
- Large datasets are handled with Realm's lazy loading |
|
||||||
- Chart rendering is optimized for large data sets |
|
||||||
- Background processing uses Kotlin Coroutines |
|
||||||
|
|
||||||
## Security & Privacy |
|
||||||
- Sensitive data is encrypted in Realm database |
|
||||||
- Crash logging excludes personal information |
|
||||||
- Backup functionality includes data encryption |
|
||||||
|
Before Width: | Height: | Size: 161 KiB |
@ -1,75 +0,0 @@ |
|||||||
package net.pokeranalytics.android.api |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import kotlinx.coroutines.CoroutineScope |
|
||||||
import kotlinx.coroutines.Dispatchers |
|
||||||
import kotlinx.coroutines.async |
|
||||||
import net.pokeranalytics.android.util.CrashLogging |
|
||||||
import net.pokeranalytics.android.util.extensions.isNetworkAvailable |
|
||||||
import okhttp3.MediaType |
|
||||||
import okhttp3.MultipartBody |
|
||||||
import okhttp3.RequestBody |
|
||||||
import retrofit2.Call |
|
||||||
import retrofit2.Retrofit |
|
||||||
import retrofit2.http.Multipart |
|
||||||
import retrofit2.http.POST |
|
||||||
import retrofit2.http.Part |
|
||||||
import timber.log.Timber |
|
||||||
|
|
||||||
object RetrofitClient { |
|
||||||
private const val BASE_URL = "https://www.pokeranalytics.net/backup/" |
|
||||||
fun getClient(): Retrofit = |
|
||||||
Retrofit.Builder() |
|
||||||
.baseUrl(BASE_URL) |
|
||||||
.build() |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
class BackupService { |
|
||||||
private val retrofit = RetrofitClient.getClient() |
|
||||||
val backupApi: MyBackupApi = retrofit.create(MyBackupApi::class.java) |
|
||||||
} |
|
||||||
|
|
||||||
interface MyBackupApi { |
|
||||||
@Multipart |
|
||||||
@POST("send") |
|
||||||
fun postFile(@Part mail: MultipartBody.Part, @Part fileBody: MultipartBody.Part): Call<Void> |
|
||||||
} |
|
||||||
|
|
||||||
object BackupApi { |
|
||||||
|
|
||||||
private val service = BackupService() |
|
||||||
|
|
||||||
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send |
|
||||||
suspend fun backupFile(context: Context, mail: String, fileName: String, fileContent: String): Boolean { |
|
||||||
|
|
||||||
val filePart = MultipartBody.Part.createFormData( |
|
||||||
"file", |
|
||||||
fileName, |
|
||||||
RequestBody.create(MediaType.parse("text/csv"), fileContent) |
|
||||||
) |
|
||||||
|
|
||||||
val mailPart = MultipartBody.Part.createFormData("recipient", mail) |
|
||||||
|
|
||||||
return if (context.isNetworkAvailable()) { |
|
||||||
var success = false |
|
||||||
val job = CoroutineScope(context = Dispatchers.IO).async { |
|
||||||
success = try { |
|
||||||
val response = service.backupApi.postFile(mailPart, filePart).execute() |
|
||||||
Timber.d("response code = ${response.code()}") |
|
||||||
Timber.d("success = ${response.isSuccessful}") |
|
||||||
true |
|
||||||
} catch (e: Exception) { |
|
||||||
Timber.d("!!! backup failed: ${e.message}") |
|
||||||
CrashLogging.logException(e) |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
||||||
job.await() |
|
||||||
return success |
|
||||||
} else { |
|
||||||
false |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,242 +0,0 @@ |
|||||||
package net.pokeranalytics.android.api |
|
||||||
|
|
||||||
import com.android.volley.* |
|
||||||
import com.android.volley.toolbox.HttpHeaderParser |
|
||||||
import java.io.* |
|
||||||
|
|
||||||
|
|
||||||
open class VolleyMultipartRequest : Request<NetworkResponse?> { |
|
||||||
|
|
||||||
private val twoHyphens = "--" |
|
||||||
private val lineEnd = "\r\n" |
|
||||||
private val boundary = "apiclient-" + System.currentTimeMillis() |
|
||||||
private var mListener: Response.Listener<NetworkResponse> |
|
||||||
private var mErrorListener: Response.ErrorListener |
|
||||||
private var mHeaders: Map<String, String>? = null |
|
||||||
private var byteData: Map<String, DataPart>? = null |
|
||||||
|
|
||||||
/** |
|
||||||
* Default constructor with predefined header and post method. |
|
||||||
* |
|
||||||
* @param url request destination |
|
||||||
* @param headers predefined custom header |
|
||||||
* @param listener on success achieved 200 code from request |
|
||||||
* @param errorListener on error http or library timeout |
|
||||||
*/ |
|
||||||
constructor( |
|
||||||
url: String?, headers: Map<String, String>?, |
|
||||||
byteData: Map<String, DataPart>, |
|
||||||
listener: Response.Listener<NetworkResponse>, |
|
||||||
errorListener: Response.ErrorListener |
|
||||||
) : super(Method.POST, url, errorListener) { |
|
||||||
mListener = listener |
|
||||||
this.mErrorListener = errorListener |
|
||||||
mHeaders = headers |
|
||||||
this.byteData = byteData |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Constructor with option method and default header configuration. |
|
||||||
* |
|
||||||
* @param method method for now accept POST and GET only |
|
||||||
* @param url request destination |
|
||||||
* @param listener on success event handler |
|
||||||
* @param errorListener on error event handler |
|
||||||
*/ |
|
||||||
constructor( |
|
||||||
method: Int, url: String?, |
|
||||||
listener: Response.Listener<NetworkResponse>, |
|
||||||
errorListener: Response.ErrorListener |
|
||||||
) : super(method, url, errorListener) { |
|
||||||
mListener = listener |
|
||||||
this.mErrorListener = errorListener |
|
||||||
} |
|
||||||
|
|
||||||
@Throws(AuthFailureError::class) |
|
||||||
override fun getHeaders(): Map<String, String> { |
|
||||||
return if (mHeaders != null) mHeaders!! else super.getHeaders() |
|
||||||
} |
|
||||||
|
|
||||||
override fun getBodyContentType(): String { |
|
||||||
return "multipart/form-data;boundary=$boundary" |
|
||||||
} |
|
||||||
|
|
||||||
@Throws(AuthFailureError::class) |
|
||||||
override fun getBody(): ByteArray? { |
|
||||||
val bos = ByteArrayOutputStream() |
|
||||||
val dos = DataOutputStream(bos) |
|
||||||
try { |
|
||||||
// populate text payload |
|
||||||
val params = params |
|
||||||
if (params != null && params.isNotEmpty()) { |
|
||||||
textParse(dos, params, paramsEncoding) |
|
||||||
} |
|
||||||
|
|
||||||
// populate data byte payload |
|
||||||
val data = |
|
||||||
byteData |
|
||||||
if (data != null && data.isNotEmpty()) { |
|
||||||
dataParse(dos, data) |
|
||||||
} |
|
||||||
|
|
||||||
// close multipart form data after text and file data |
|
||||||
dos.writeBytes(twoHyphens + boundary + twoHyphens + lineEnd) |
|
||||||
return bos.toByteArray() |
|
||||||
} catch (e: IOException) { |
|
||||||
e.printStackTrace() |
|
||||||
} |
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
override fun parseNetworkResponse(response: NetworkResponse?): Response<NetworkResponse?> { |
|
||||||
return try { |
|
||||||
Response.success( |
|
||||||
response, |
|
||||||
HttpHeaderParser.parseCacheHeaders(response) |
|
||||||
) |
|
||||||
} catch (e: Exception) { |
|
||||||
Response.error(ParseError(e)) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun deliverResponse(response: NetworkResponse?) { |
|
||||||
mListener.onResponse(response) |
|
||||||
} |
|
||||||
|
|
||||||
override fun deliverError(error: VolleyError?) { |
|
||||||
mErrorListener.onErrorResponse(error) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Parse string map into data output stream by key and value. |
|
||||||
* |
|
||||||
* @param dataOutputStream data output stream handle string parsing |
|
||||||
* @param params string inputs collection |
|
||||||
* @param encoding encode the inputs, default UTF-8 |
|
||||||
* @throws IOException |
|
||||||
*/ |
|
||||||
@Throws(IOException::class) |
|
||||||
private fun textParse( |
|
||||||
dataOutputStream: DataOutputStream, |
|
||||||
params: Map<String, String>, |
|
||||||
encoding: String |
|
||||||
) { |
|
||||||
try { |
|
||||||
for ((key, value) in params) { |
|
||||||
buildTextPart(dataOutputStream, key, value) |
|
||||||
} |
|
||||||
} catch (uee: UnsupportedEncodingException) { |
|
||||||
throw RuntimeException("Encoding not supported: $encoding", uee) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Parse data into data output stream. |
|
||||||
* |
|
||||||
* @param dataOutputStream data output stream handle file attachment |
|
||||||
* @param data loop through data |
|
||||||
* @throws IOException |
|
||||||
*/ |
|
||||||
@Throws(IOException::class) |
|
||||||
private fun dataParse(dataOutputStream: DataOutputStream, data: Map<String, DataPart>) { |
|
||||||
for ((key, value) in data) { |
|
||||||
buildDataPart(dataOutputStream, value, key) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Write string data into header and data output stream. |
|
||||||
* |
|
||||||
* @param dataOutputStream data output stream handle string parsing |
|
||||||
* @param parameterName name of input |
|
||||||
* @param parameterValue value of input |
|
||||||
* @throws IOException |
|
||||||
*/ |
|
||||||
@Throws(IOException::class) |
|
||||||
private fun buildTextPart( |
|
||||||
dataOutputStream: DataOutputStream, |
|
||||||
parameterName: String, |
|
||||||
parameterValue: String |
|
||||||
) { |
|
||||||
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd) |
|
||||||
dataOutputStream.writeBytes("Content-Disposition: form-data; name=\"$parameterName\"$lineEnd") |
|
||||||
//dataOutputStream.writeBytes("Content-Type: text/plain; charset=UTF-8" + lineEnd); |
|
||||||
dataOutputStream.writeBytes(lineEnd) |
|
||||||
dataOutputStream.writeBytes(parameterValue + lineEnd) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Write data file into header and data output stream. |
|
||||||
* |
|
||||||
* @param dataOutputStream data output stream handle data parsing |
|
||||||
* @param dataFile data byte as DataPart from collection |
|
||||||
* @param inputName name of data input |
|
||||||
* @throws IOException |
|
||||||
*/ |
|
||||||
@Throws(IOException::class) |
|
||||||
private fun buildDataPart( |
|
||||||
dataOutputStream: DataOutputStream, |
|
||||||
dataFile: DataPart, |
|
||||||
inputName: String |
|
||||||
) { |
|
||||||
dataOutputStream.writeBytes(twoHyphens + boundary + lineEnd) |
|
||||||
dataOutputStream.writeBytes( |
|
||||||
"Content-Disposition: form-data; name=\"" + |
|
||||||
inputName + "\"; filename=\"" + dataFile.fileName + "\"" + lineEnd |
|
||||||
) |
|
||||||
if (dataFile.type != null && !dataFile.type!!.trim { it <= ' ' }.isEmpty()) { |
|
||||||
dataOutputStream.writeBytes("Content-Type: " + dataFile.type + lineEnd) |
|
||||||
} |
|
||||||
dataOutputStream.writeBytes(lineEnd) |
|
||||||
val fileInputStream = ByteArrayInputStream(dataFile.content) |
|
||||||
var bytesAvailable: Int = fileInputStream.available() |
|
||||||
val maxBufferSize = 1024 * 1024 |
|
||||||
var bufferSize = Math.min(bytesAvailable, maxBufferSize) |
|
||||||
val buffer = ByteArray(bufferSize) |
|
||||||
var bytesRead: Int = fileInputStream.read(buffer, 0, bufferSize) |
|
||||||
while (bytesRead > 0) { |
|
||||||
dataOutputStream.write(buffer, 0, bufferSize) |
|
||||||
bytesAvailable = fileInputStream.available() |
|
||||||
bufferSize = Math.min(bytesAvailable, maxBufferSize) |
|
||||||
bytesRead = fileInputStream.read(buffer, 0, bufferSize) |
|
||||||
} |
|
||||||
dataOutputStream.writeBytes(lineEnd) |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Simple data container use for passing byte file |
|
||||||
*/ |
|
||||||
class DataPart { |
|
||||||
|
|
||||||
var fileName: String? = null |
|
||||||
|
|
||||||
var content: ByteArray? = null |
|
||||||
|
|
||||||
var type: String? = null |
|
||||||
|
|
||||||
/** |
|
||||||
* Constructor with data. |
|
||||||
* |
|
||||||
* @param name label of data |
|
||||||
* @param data byte data |
|
||||||
*/ |
|
||||||
constructor(name: String?, data: ByteArray) { |
|
||||||
fileName = name |
|
||||||
content = data |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Constructor with mime data type. |
|
||||||
* |
|
||||||
* @param name label of data |
|
||||||
* @param data byte data |
|
||||||
* @param mimeType mime data like "image/jpeg" |
|
||||||
*/ |
|
||||||
constructor(name: String?, data: ByteArray, mimeType: String?) { |
|
||||||
fileName = name |
|
||||||
content = data |
|
||||||
type = mimeType |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,99 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.activity |
|
||||||
|
|
||||||
import android.Manifest |
|
||||||
import android.content.Context |
|
||||||
import android.content.Intent |
|
||||||
import android.net.Uri |
|
||||||
import android.os.Bundle |
|
||||||
import androidx.fragment.app.FragmentActivity |
|
||||||
import io.realm.Realm |
|
||||||
import net.pokeranalytics.android.R |
|
||||||
import net.pokeranalytics.android.exceptions.PAIllegalStateException |
|
||||||
import net.pokeranalytics.android.ui.activity.components.BaseActivity |
|
||||||
import net.pokeranalytics.android.ui.activity.components.RequestCode |
|
||||||
import net.pokeranalytics.android.ui.activity.components.ResultCode |
|
||||||
import net.pokeranalytics.android.ui.extensions.showAlertDialog |
|
||||||
import net.pokeranalytics.android.ui.extensions.toast |
|
||||||
import net.pokeranalytics.android.util.copyStreamToFile |
|
||||||
import timber.log.Timber |
|
||||||
import java.io.File |
|
||||||
|
|
||||||
class DatabaseCopyActivity : BaseActivity() { |
|
||||||
|
|
||||||
private lateinit var fileURI: Uri |
|
||||||
|
|
||||||
enum class IntentKey(val keyName: String) { |
|
||||||
URI("uri") |
|
||||||
} |
|
||||||
|
|
||||||
companion object { |
|
||||||
|
|
||||||
/** |
|
||||||
* Create a new instance for result |
|
||||||
*/ |
|
||||||
fun newInstanceForResult(context: FragmentActivity, uri: Uri) { |
|
||||||
context.startActivityForResult(getIntent(context, uri), RequestCode.IMPORT.value) |
|
||||||
} |
|
||||||
|
|
||||||
private fun getIntent(context: Context, uri: Uri): Intent { |
|
||||||
Timber.d("getIntent") |
|
||||||
val intent = Intent(context, DatabaseCopyActivity::class.java) |
|
||||||
intent.putExtra(IntentKey.URI.keyName, uri) |
|
||||||
return intent |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
|
|
||||||
Timber.d("onCreate") |
|
||||||
|
|
||||||
intent?.data?.let { |
|
||||||
this.fileURI = it |
|
||||||
} ?: run { |
|
||||||
this.fileURI = intent.getParcelableExtra(IntentKey.URI.keyName) ?: throw PAIllegalStateException("Uri not found") |
|
||||||
} |
|
||||||
|
|
||||||
// setContentView(R.layout.activity_import) |
|
||||||
requestImportConfirmation() |
|
||||||
} |
|
||||||
|
|
||||||
private fun initUI() { |
|
||||||
|
|
||||||
askForPermission(arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE), RequestCode.PERMISSION_WRITE_EXTERNAL_STORAGE.value) { |
|
||||||
val path = Realm.getDefaultInstance().path |
|
||||||
contentResolver.openInputStream(fileURI)?.let { inputStream -> |
|
||||||
val destination = File(path) |
|
||||||
inputStream.copyStreamToFile(destination) |
|
||||||
toast("Please restart app") |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { |
|
||||||
super.onActivityResult(requestCode, resultCode, data) |
|
||||||
|
|
||||||
when (requestCode) { |
|
||||||
RequestCode.IMPORT.value -> { |
|
||||||
if (resultCode == ResultCode.IMPORT_UNRECOGNIZED_FORMAT.value) { |
|
||||||
showAlertDialog(context = this, messageResId = R.string.unknown_import_format_popup_message, positiveAction = { |
|
||||||
finish() |
|
||||||
}) |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Import |
|
||||||
|
|
||||||
private fun requestImportConfirmation() { |
|
||||||
|
|
||||||
showAlertDialog(context = this, title = R.string.import_confirmation, showCancelButton = true, positiveAction = { |
|
||||||
initUI() |
|
||||||
}, negativeAction = { |
|
||||||
finish() |
|
||||||
}) |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,187 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.activity.components |
|
||||||
|
|
||||||
import android.Manifest |
|
||||||
import android.app.Activity |
|
||||||
import android.content.ContentValues |
|
||||||
import android.content.Intent |
|
||||||
import android.content.pm.PackageManager |
|
||||||
import android.os.Build |
|
||||||
import android.os.Bundle |
|
||||||
import android.provider.MediaStore |
|
||||||
import android.widget.Toast |
|
||||||
import androidx.camera.core.CameraSelector |
|
||||||
import androidx.camera.core.ImageCapture |
|
||||||
import androidx.camera.core.ImageCaptureException |
|
||||||
import androidx.camera.core.Preview |
|
||||||
import androidx.camera.lifecycle.ProcessCameraProvider |
|
||||||
import androidx.core.app.ActivityCompat |
|
||||||
import androidx.core.content.ContextCompat |
|
||||||
import androidx.fragment.app.Fragment |
|
||||||
import net.pokeranalytics.android.databinding.ActivityCameraBinding |
|
||||||
import timber.log.Timber |
|
||||||
import java.text.SimpleDateFormat |
|
||||||
import java.util.* |
|
||||||
import java.util.concurrent.ExecutorService |
|
||||||
import java.util.concurrent.Executors |
|
||||||
|
|
||||||
class CameraActivity : BaseActivity() { |
|
||||||
|
|
||||||
companion object { |
|
||||||
|
|
||||||
const val IMAGE_URI = "image_uri" |
|
||||||
|
|
||||||
fun newInstanceForResult(fragment: Fragment, code: RequestCode) { |
|
||||||
val intent = Intent(fragment.requireContext(), CameraActivity::class.java) |
|
||||||
fragment.startActivityForResult(intent, code.value) |
|
||||||
} |
|
||||||
|
|
||||||
private const val FILENAME_FORMAT = "yyyy-MM-dd-HH-mm-ss-SSS" |
|
||||||
private const val REQUEST_CODE_PERMISSIONS = 10 |
|
||||||
private val REQUIRED_PERMISSIONS = |
|
||||||
mutableListOf ( |
|
||||||
Manifest.permission.CAMERA |
|
||||||
).apply { |
|
||||||
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.P) { |
|
||||||
add(Manifest.permission.WRITE_EXTERNAL_STORAGE) |
|
||||||
} |
|
||||||
}.toTypedArray() |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
private lateinit var viewBinding: ActivityCameraBinding |
|
||||||
|
|
||||||
private lateinit var cameraExecutor: ExecutorService |
|
||||||
private var imageCapture: ImageCapture? = null |
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) { |
|
||||||
super.onCreate(savedInstanceState) |
|
||||||
|
|
||||||
this.viewBinding = ActivityCameraBinding.inflate(layoutInflater) |
|
||||||
setContentView(viewBinding.root) |
|
||||||
|
|
||||||
if (allPermissionsGranted()) { |
|
||||||
cameraExecutor = Executors.newSingleThreadExecutor() |
|
||||||
startCamera() |
|
||||||
} else { |
|
||||||
ActivityCompat.requestPermissions( |
|
||||||
this, REQUIRED_PERMISSIONS, REQUEST_CODE_PERMISSIONS) |
|
||||||
} |
|
||||||
|
|
||||||
viewBinding.imageCaptureButton.setOnClickListener { takePhoto() } |
|
||||||
cameraExecutor = Executors.newSingleThreadExecutor() |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
override fun onDestroy() { |
|
||||||
super.onDestroy() |
|
||||||
this.cameraExecutor.shutdown() |
|
||||||
} |
|
||||||
|
|
||||||
override fun onRequestPermissionsResult( |
|
||||||
requestCode: Int, |
|
||||||
permissions: Array<out String>, |
|
||||||
grantResults: IntArray |
|
||||||
) { |
|
||||||
super.onRequestPermissionsResult(requestCode, permissions, grantResults) |
|
||||||
|
|
||||||
if (requestCode == REQUEST_CODE_PERMISSIONS) { |
|
||||||
if (allPermissionsGranted()) { |
|
||||||
startCamera() |
|
||||||
} else { |
|
||||||
Toast.makeText(this, |
|
||||||
"Permissions not granted by the user.", |
|
||||||
Toast.LENGTH_SHORT).show() |
|
||||||
finish() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun allPermissionsGranted() = REQUIRED_PERMISSIONS.all { |
|
||||||
ContextCompat.checkSelfPermission( |
|
||||||
baseContext, it) == PackageManager.PERMISSION_GRANTED |
|
||||||
} |
|
||||||
|
|
||||||
private fun startCamera() { |
|
||||||
val cameraProviderFuture = ProcessCameraProvider.getInstance(this) |
|
||||||
|
|
||||||
cameraProviderFuture.addListener({ |
|
||||||
// Used to bind the lifecycle of cameras to the lifecycle owner |
|
||||||
val cameraProvider: ProcessCameraProvider = cameraProviderFuture.get() |
|
||||||
|
|
||||||
// Preview |
|
||||||
val preview = Preview.Builder() |
|
||||||
.build() |
|
||||||
.also { |
|
||||||
it.setSurfaceProvider(this.viewBinding.viewFinder.surfaceProvider) |
|
||||||
} |
|
||||||
|
|
||||||
imageCapture = ImageCapture.Builder().build() |
|
||||||
|
|
||||||
// Select back camera as a default |
|
||||||
val cameraSelector = CameraSelector.DEFAULT_BACK_CAMERA |
|
||||||
|
|
||||||
try { |
|
||||||
// Unbind use cases before rebinding |
|
||||||
cameraProvider.unbindAll() |
|
||||||
|
|
||||||
// Bind use cases to camera |
|
||||||
cameraProvider.bindToLifecycle( |
|
||||||
this, cameraSelector, preview, imageCapture) |
|
||||||
|
|
||||||
} catch(exc: Exception) { |
|
||||||
Timber.e(exc) |
|
||||||
} |
|
||||||
|
|
||||||
}, ContextCompat.getMainExecutor(this)) |
|
||||||
} |
|
||||||
|
|
||||||
|
|
||||||
private fun takePhoto() { |
|
||||||
// Get a stable reference of the modifiable image capture use case |
|
||||||
val imageCapture = imageCapture ?: return |
|
||||||
|
|
||||||
// Create time stamped name and MediaStore entry. |
|
||||||
val name = SimpleDateFormat(FILENAME_FORMAT, Locale.US) |
|
||||||
.format(System.currentTimeMillis()) |
|
||||||
val contentValues = ContentValues().apply { |
|
||||||
put(MediaStore.MediaColumns.DISPLAY_NAME, name) |
|
||||||
put(MediaStore.MediaColumns.MIME_TYPE, "image/jpeg") |
|
||||||
if(Build.VERSION.SDK_INT > Build.VERSION_CODES.P) { |
|
||||||
put(MediaStore.Images.Media.RELATIVE_PATH, "Pictures/CameraX-Image") |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
// Create output options object which contains file + metadata |
|
||||||
val outputOptions = ImageCapture.OutputFileOptions |
|
||||||
.Builder(contentResolver, |
|
||||||
MediaStore.Images.Media.EXTERNAL_CONTENT_URI, |
|
||||||
contentValues) |
|
||||||
.build() |
|
||||||
|
|
||||||
// Set up image capture listener, which is triggered after photo has |
|
||||||
// been taken |
|
||||||
imageCapture.takePicture( |
|
||||||
outputOptions, |
|
||||||
ContextCompat.getMainExecutor(this), |
|
||||||
object : ImageCapture.OnImageSavedCallback { |
|
||||||
override fun onError(e: ImageCaptureException) { |
|
||||||
Timber.e(e) |
|
||||||
} |
|
||||||
|
|
||||||
override fun onImageSaved(output: ImageCapture.OutputFileResults) { |
|
||||||
|
|
||||||
// val msg = "Photo capture succeeded: ${output.savedUri}" |
|
||||||
// Toast.makeText(baseContext, msg, Toast.LENGTH_SHORT).show() |
|
||||||
// Timber.d(msg) |
|
||||||
|
|
||||||
val intent = Intent() |
|
||||||
intent.putExtra(IMAGE_URI, output.savedUri.toString()) |
|
||||||
setResult(Activity.RESULT_OK, intent) |
|
||||||
|
|
||||||
finish() |
|
||||||
} |
|
||||||
} |
|
||||||
) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,153 +0,0 @@ |
|||||||
package net.pokeranalytics.android.ui.modules.data |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import io.realm.Realm |
|
||||||
import io.realm.kotlin.where |
|
||||||
import net.pokeranalytics.android.model.realm.Comment |
|
||||||
import net.pokeranalytics.android.model.realm.Player |
|
||||||
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.RowViewType |
|
||||||
import net.pokeranalytics.android.ui.view.rows.CustomizableRowRepresentable |
|
||||||
import net.pokeranalytics.android.ui.view.rows.PlayerPropertiesRow |
|
||||||
import net.pokeranalytics.android.ui.view.rows.SeparatorRow |
|
||||||
import net.pokeranalytics.android.ui.viewmodel.DataManagerViewModel |
|
||||||
import net.pokeranalytics.android.util.NULL_TEXT |
|
||||||
import net.pokeranalytics.android.util.extensions.isSameDay |
|
||||||
import net.pokeranalytics.android.util.extensions.mediumDate |
|
||||||
import java.util.* |
|
||||||
|
|
||||||
class PlayerDataViewModel : DataManagerViewModel(), StaticRowRepresentableDataSource { |
|
||||||
|
|
||||||
private var rowRepresentation: List<RowRepresentable> = mutableListOf() |
|
||||||
|
|
||||||
private var commentsToDelete: ArrayList<Comment> = ArrayList() |
|
||||||
|
|
||||||
var selectedTab: Int = 0 |
|
||||||
set(value) { |
|
||||||
field = value |
|
||||||
this.updateRowRepresentation() |
|
||||||
} |
|
||||||
|
|
||||||
private val player: Player |
|
||||||
get() { |
|
||||||
return this.item as Player |
|
||||||
} |
|
||||||
|
|
||||||
override fun adapterRows(): List<RowRepresentable> { |
|
||||||
return this.rowRepresentation |
|
||||||
} |
|
||||||
|
|
||||||
override fun charSequenceForRow( |
|
||||||
row: RowRepresentable, |
|
||||||
context: Context, |
|
||||||
tag: Int |
|
||||||
): CharSequence { |
|
||||||
return when (row) { |
|
||||||
PlayerPropertiesRow.NAME -> this.player.name.ifEmpty { NULL_TEXT } |
|
||||||
else -> return super.charSequenceForRow(row, context, 0) |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
fun updateRowRepresentation() { |
|
||||||
this.rowRepresentation = this.updatedRowRepresentationForCurrentState() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Update the row representation |
|
||||||
*/ |
|
||||||
private fun updatedRowRepresentationForCurrentState(): List<RowRepresentable> { |
|
||||||
val rows = ArrayList<RowRepresentable>() |
|
||||||
rows.add(PlayerPropertiesRow.IMAGE) |
|
||||||
rows.add(PlayerPropertiesRow.NAME) |
|
||||||
rows.add(PlayerPropertiesRow.SUMMARY) |
|
||||||
|
|
||||||
val realm = Realm.getDefaultInstance() |
|
||||||
val hands = this.player.hands(realm) |
|
||||||
realm.close() |
|
||||||
|
|
||||||
if (this.player.comments.isNotEmpty() || hands.isNotEmpty()) { |
|
||||||
rows.add(PlayerPropertiesRow.TAB_SELECTOR) |
|
||||||
} |
|
||||||
|
|
||||||
when(this.selectedTab) { |
|
||||||
0 -> this.addCommentSection(rows) |
|
||||||
1 -> rows.addAll(hands) |
|
||||||
} |
|
||||||
|
|
||||||
return rows |
|
||||||
} |
|
||||||
|
|
||||||
private fun addCommentSection(rows: ArrayList<RowRepresentable>) { |
|
||||||
|
|
||||||
if (this.player.comments.size > 0) { |
|
||||||
|
|
||||||
val currentCommentCalendar = Calendar.getInstance() |
|
||||||
val currentDateCalendar = Calendar.getInstance() |
|
||||||
|
|
||||||
val commentsToDisplay = ArrayList<Comment>() |
|
||||||
commentsToDisplay.addAll(this.player.comments) |
|
||||||
commentsToDisplay.sortByDescending { it.date } |
|
||||||
|
|
||||||
commentsToDisplay.forEachIndexed { index, comment -> |
|
||||||
currentCommentCalendar.time = comment.date |
|
||||||
|
|
||||||
if (!currentCommentCalendar.isSameDay(currentDateCalendar) || index == 0) { |
|
||||||
currentDateCalendar.time = currentCommentCalendar.time |
|
||||||
// Adds day sub section |
|
||||||
rows.add(CustomizableRowRepresentable(RowViewType.HEADER_SUBTITLE, title = currentDateCalendar.time.mediumDate())) |
|
||||||
} |
|
||||||
|
|
||||||
// Adds comment |
|
||||||
rows.add(comment) |
|
||||||
} |
|
||||||
rows.add(SeparatorRow()) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Add an entry |
|
||||||
*/ |
|
||||||
fun addComment(): Comment { |
|
||||||
val entry = Comment() |
|
||||||
this.player.comments.add(entry) |
|
||||||
updateRowRepresentation() |
|
||||||
return entry |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Delete an entry |
|
||||||
*/ |
|
||||||
fun deleteComment(comment: Comment) { |
|
||||||
commentsToDelete.add(comment) |
|
||||||
this.player.comments.remove(comment) |
|
||||||
updateRowRepresentation() |
|
||||||
} |
|
||||||
|
|
||||||
/** |
|
||||||
* Clean up deleted entries |
|
||||||
*/ |
|
||||||
fun cleanupComments() { // called when saving the custom field |
|
||||||
val realm = Realm.getDefaultInstance() |
|
||||||
realm.executeTransaction { |
|
||||||
this.commentsToDelete.forEach { // entries are out of realm |
|
||||||
realm.where<Comment>().equalTo("id", it.id).findFirst()?.deleteFromRealm() |
|
||||||
} |
|
||||||
} |
|
||||||
realm.close() |
|
||||||
this.commentsToDelete.clear() |
|
||||||
} |
|
||||||
|
|
||||||
override fun editDescriptors(row: RowRepresentable): List<RowRepresentableEditDescriptor>? { |
|
||||||
|
|
||||||
when (row) { |
|
||||||
PlayerPropertiesRow.NAME -> return row.editingDescriptors(mapOf("defaultValue" to this.player.name)) |
|
||||||
PlayerPropertiesRow.SUMMARY -> return row.editingDescriptors(mapOf("defaultValue" to this.player.summary)) |
|
||||||
} |
|
||||||
|
|
||||||
return null |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,73 +0,0 @@ |
|||||||
package net.pokeranalytics.android.util |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import androidx.work.Data |
|
||||||
import androidx.work.ExistingWorkPolicy |
|
||||||
import androidx.work.OneTimeWorkRequestBuilder |
|
||||||
import androidx.work.WorkManager |
|
||||||
import io.realm.Realm |
|
||||||
import io.realm.RealmResults |
|
||||||
import net.pokeranalytics.android.BuildConfig |
|
||||||
import net.pokeranalytics.android.model.realm.Session |
|
||||||
import net.pokeranalytics.android.model.realm.Transaction |
|
||||||
import net.pokeranalytics.android.util.csv.DataType |
|
||||||
import timber.log.Timber |
|
||||||
import java.util.concurrent.TimeUnit |
|
||||||
|
|
||||||
class BackupOperator(var context: Context) { |
|
||||||
|
|
||||||
private var sessions: RealmResults<Session>? = null |
|
||||||
private var transactions: RealmResults<Transaction>? = null |
|
||||||
|
|
||||||
private var sessionsInitialized = false |
|
||||||
private var transactionsInitialized = false |
|
||||||
|
|
||||||
private val realm = Realm.getDefaultInstance() |
|
||||||
|
|
||||||
init { |
|
||||||
|
|
||||||
this.sessions = this.realm.where(Session::class.java).findAllAsync() |
|
||||||
this.sessions?.addChangeListener { _ -> |
|
||||||
if (this.sessionsInitialized) { |
|
||||||
Preferences.getBackupEmail(context)?.let { |
|
||||||
backupDataType(DataType.SESSION) |
|
||||||
} |
|
||||||
} |
|
||||||
this.sessionsInitialized = true |
|
||||||
} |
|
||||||
|
|
||||||
this.transactions = this.realm.where(Transaction::class.java).findAllAsync() |
|
||||||
this.transactions?.addChangeListener { _ -> |
|
||||||
if (this.transactionsInitialized) { |
|
||||||
Preferences.getBackupEmail(context)?.let { |
|
||||||
backupDataType(DataType.TRANSACTION) |
|
||||||
} |
|
||||||
} |
|
||||||
this.transactionsInitialized = true |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
private fun backupDataType(dataType: DataType) { |
|
||||||
val data = Data.Builder() |
|
||||||
.putInt(BackupWorker.ParamKeys.DATA.value, dataType.ordinal) |
|
||||||
|
|
||||||
var duration = 10L |
|
||||||
var unit = TimeUnit.HOURS |
|
||||||
if (BuildConfig.DEBUG) { |
|
||||||
duration = 1L |
|
||||||
unit = TimeUnit.SECONDS |
|
||||||
} |
|
||||||
|
|
||||||
val backupWorker = OneTimeWorkRequestBuilder<BackupWorker>() |
|
||||||
.setInitialDelay(duration, unit) |
|
||||||
.setInputData(data.build()) |
|
||||||
.addTag(dataType.workId) |
|
||||||
.build() |
|
||||||
|
|
||||||
Timber.d(">>> create backupTask") |
|
||||||
|
|
||||||
WorkManager.getInstance(context).enqueueUniqueWork(dataType.workId, ExistingWorkPolicy.REPLACE, backupWorker) |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,86 +0,0 @@ |
|||||||
package net.pokeranalytics.android.util |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import androidx.work.Worker |
|
||||||
import androidx.work.WorkerParameters |
|
||||||
import io.realm.Realm |
|
||||||
import kotlinx.coroutines.CoroutineScope |
|
||||||
import kotlinx.coroutines.Dispatchers |
|
||||||
import kotlinx.coroutines.launch |
|
||||||
import net.pokeranalytics.android.api.BackupApi |
|
||||||
import net.pokeranalytics.android.model.realm.Session |
|
||||||
import net.pokeranalytics.android.model.realm.Transaction |
|
||||||
import net.pokeranalytics.android.util.csv.DataType |
|
||||||
import net.pokeranalytics.android.util.csv.ProductCSVDescriptors |
|
||||||
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted |
|
||||||
import timber.log.Timber |
|
||||||
import java.util.* |
|
||||||
|
|
||||||
|
|
||||||
class BackupWorker(var context: Context, var params: WorkerParameters) : Worker(context, params) { |
|
||||||
|
|
||||||
enum class ParamKeys(val value: String) { |
|
||||||
DATA("title"), |
|
||||||
} |
|
||||||
|
|
||||||
override fun doWork(): Result { |
|
||||||
|
|
||||||
val data = params.inputData |
|
||||||
|
|
||||||
val dataTypeInt = data.getInt(ParamKeys.DATA.value, 0) |
|
||||||
val dataType = DataType.values()[dataTypeInt] |
|
||||||
|
|
||||||
Preferences.getBackupEmail(context)?.let { email -> |
|
||||||
val task = BackupTask(dataType, email, context) |
|
||||||
task.start() |
|
||||||
} |
|
||||||
|
|
||||||
return Result.success() |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
|
|
||||||
class BackupTask(val dataType: DataType, val email: String, val context: Context) { |
|
||||||
|
|
||||||
fun start() { |
|
||||||
when(this.dataType) { |
|
||||||
DataType.SESSION -> { |
|
||||||
backupSessions() |
|
||||||
} |
|
||||||
DataType.TRANSACTION -> { |
|
||||||
backupTransactions() |
|
||||||
} |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
private fun backupSessions() { |
|
||||||
|
|
||||||
Timber.d(">>>> backup sessions") |
|
||||||
val realm = Realm.getDefaultInstance() |
|
||||||
val sessions = realm.where(Session::class.java).findAll().sort("startDate") |
|
||||||
val csv = ProductCSVDescriptors.pokerAnalyticsAndroid6Sessions.toCSV(sessions) |
|
||||||
val fileName = "sessions_${Date().dateTimeFileFormatted}.csv" |
|
||||||
|
|
||||||
CoroutineScope(context = Dispatchers.IO).launch { |
|
||||||
val success = BackupApi.backupFile(context, email, fileName, csv) |
|
||||||
Preferences.setSessionsBackupSuccess(success, context) |
|
||||||
} |
|
||||||
realm.close() |
|
||||||
} |
|
||||||
|
|
||||||
private fun backupTransactions() { |
|
||||||
|
|
||||||
Timber.d(">>>> backup transactions") |
|
||||||
val realm = Realm.getDefaultInstance() |
|
||||||
val transactions = realm.where(Transaction::class.java).findAll().sort("date") |
|
||||||
val csv = ProductCSVDescriptors.pokerAnalyticsAndroidTransactions.toCSV(transactions) |
|
||||||
val fileName = "transactions_${Date().dateTimeFileFormatted}.csv" |
|
||||||
|
|
||||||
CoroutineScope(context = Dispatchers.IO).launch { |
|
||||||
val success = BackupApi.backupFile(context, email, fileName, csv) |
|
||||||
Preferences.setTransactionsBackupSuccess(success, context) |
|
||||||
} |
|
||||||
realm.close() |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,32 +0,0 @@ |
|||||||
package net.pokeranalytics.android.util |
|
||||||
|
|
||||||
import java.util.* |
|
||||||
|
|
||||||
enum class Language(val code: String) { |
|
||||||
ENGLISH("en"), |
|
||||||
FRENCH("fr"), |
|
||||||
GERMAN("de"), |
|
||||||
HINDI("hi"), |
|
||||||
ITALIAN("it"), |
|
||||||
JAPANESE("ja"), |
|
||||||
PORTUGUESE("pt"), |
|
||||||
RUSSIAN("ru"), |
|
||||||
CHINESE("zh"); |
|
||||||
|
|
||||||
private val localized: String |
|
||||||
get() { |
|
||||||
return Locale(code).displayLanguage |
|
||||||
} |
|
||||||
|
|
||||||
private val localName: String |
|
||||||
get() { |
|
||||||
val locale = Locale(code) |
|
||||||
return locale.getDisplayLanguage(locale) |
|
||||||
} |
|
||||||
|
|
||||||
val dualNames: String |
|
||||||
get() { |
|
||||||
return "$localized / $localName" |
|
||||||
} |
|
||||||
|
|
||||||
} |
|
||||||
@ -1,15 +0,0 @@ |
|||||||
package net.pokeranalytics.android.util.extensions |
|
||||||
|
|
||||||
import android.content.Context |
|
||||||
import android.net.ConnectivityManager |
|
||||||
import android.net.NetworkCapabilities |
|
||||||
|
|
||||||
/*** |
|
||||||
* Returns whether the network is available or not |
|
||||||
*/ |
|
||||||
fun Context.isNetworkAvailable(): Boolean { |
|
||||||
val cm = this.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager |
|
||||||
val capability = cm.getNetworkCapabilities(cm.activeNetwork) |
|
||||||
return capability?.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET) ?: false |
|
||||||
} |
|
||||||
|
|
||||||
@ -1,15 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<shape |
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android" |
|
||||||
android:shape="oval"> |
|
||||||
|
|
||||||
<!-- <solid android:color="@color/black" />--> |
|
||||||
|
|
||||||
<stroke android:color="@color/black" android:width="3dp"/> |
|
||||||
<solid android:color="@color/white"/> |
|
||||||
|
|
||||||
<size |
|
||||||
android:width="80dp" |
|
||||||
android:height="80dp"/> |
|
||||||
|
|
||||||
</shape> |
|
||||||
@ -1,74 +0,0 @@ |
|||||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||||
<vector |
|
||||||
android:height="108dp" |
|
||||||
android:width="108dp" |
|
||||||
android:viewportHeight="108" |
|
||||||
android:viewportWidth="108" |
|
||||||
xmlns:android="http://schemas.android.com/apk/res/android"> |
|
||||||
<path android:fillColor="#3DDC84" |
|
||||||
android:pathData="M0,0h108v108h-108z"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M9,0L9,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,0L19,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M29,0L29,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M39,0L39,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M49,0L49,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M59,0L59,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M69,0L69,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M79,0L79,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M89,0L89,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M99,0L99,108" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,9L108,9" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,19L108,19" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,29L108,29" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,39L108,39" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,49L108,49" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,59L108,59" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,69L108,69" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,79L108,79" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,89L108,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M0,99L108,99" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,29L89,29" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,39L89,39" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,49L89,49" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,59L89,59" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,69L89,69" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M19,79L89,79" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M29,19L29,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M39,19L39,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M49,19L49,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M59,19L59,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M69,19L69,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
<path android:fillColor="#00000000" android:pathData="M79,19L79,89" |
|
||||||
android:strokeColor="#33FFFFFF" android:strokeWidth="0.8"/> |
|
||||||
</vector> |
|
||||||
@ -1,34 +0,0 @@ |
|||||||
<?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="match_parent"> |
|
||||||
|
|
||||||
<androidx.camera.view.PreviewView |
|
||||||
android:id="@+id/viewFinder" |
|
||||||
android:layout_width="match_parent" |
|
||||||
android:layout_height="match_parent" |
|
||||||
tools:layout_editor_absoluteX="0dp" |
|
||||||
tools:layout_editor_absoluteY="0dp" /> |
|
||||||
|
|
||||||
<ImageButton |
|
||||||
android:id="@+id/image_capture_button" |
|
||||||
android:src="@drawable/circle_border" |
|
||||||
style="@style/PokerAnalyticsTheme.TransparentButton" |
|
||||||
android:layout_width="80dp" |
|
||||||
android:layout_height="80dp" |
|
||||||
android:layout_marginBottom="20dp" |
|
||||||
app:layout_constraintBottom_toBottomOf="parent" |
|
||||||
app:layout_constraintEnd_toEndOf="parent" |
|
||||||
app:layout_constraintStart_toStartOf="parent" /> |
|
||||||
|
|
||||||
<androidx.constraintlayout.widget.Guideline |
|
||||||
android:id="@+id/vertical_centerline" |
|
||||||
android:layout_width="wrap_content" |
|
||||||
android:layout_height="wrap_content" |
|
||||||
android:orientation="vertical" |
|
||||||
app:layout_constraintGuide_percent=".50" /> |
|
||||||
|
|
||||||
</androidx.constraintlayout.widget.ConstraintLayout> |
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue