parent
d4078032a4
commit
703de02f26
@ -0,0 +1,62 @@ |
||||
package net.pokeranalytics.android.api |
||||
|
||||
import kotlinx.coroutines.CoroutineScope |
||||
import kotlinx.coroutines.Dispatchers |
||||
import kotlinx.coroutines.launch |
||||
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 { |
||||
|
||||
val service = BackupService() |
||||
|
||||
// curl -F recipient=laurent@staxriver.com -F file=@test.txt https://www.pokeranalytics.net/backup/send |
||||
fun backupFile(mail: String, fileName: String, fileContent: String) { |
||||
|
||||
val filePart = MultipartBody.Part.createFormData( |
||||
"file", |
||||
fileName, |
||||
RequestBody.create(MediaType.parse("text/csv"), fileContent) |
||||
) |
||||
|
||||
val mailPart = MultipartBody.Part.createFormData("recipient", mail) |
||||
|
||||
CoroutineScope(context = Dispatchers.IO).launch { |
||||
|
||||
val response = service.backupApi.postFile(mailPart, filePart).execute() |
||||
Timber.d("response code = ${response.code()}") |
||||
Timber.d("success = ${response.isSuccessful}") |
||||
|
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,242 @@ |
||||
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 |
||||
} |
||||
} |
||||
|
||||
} |
||||
@ -0,0 +1,76 @@ |
||||
package net.pokeranalytics.android.util |
||||
|
||||
import android.content.Context |
||||
import io.realm.Realm |
||||
import io.realm.RealmResults |
||||
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.ProductCSVDescriptors |
||||
import net.pokeranalytics.android.util.extensions.dateTimeFileFormatted |
||||
import timber.log.Timber |
||||
import java.util.* |
||||
|
||||
class BackupOperator(var context: Context) { |
||||
|
||||
private var sessions: RealmResults<Session>? = null |
||||
private var transactions: RealmResults<Transaction>? = null |
||||
|
||||
private var sessionsChanged = false |
||||
private var transactionsChanged = false |
||||
|
||||
private val realm = Realm.getDefaultInstance() |
||||
|
||||
init { |
||||
|
||||
this.sessions = this.realm.where(Session::class.java).findAll() |
||||
this.sessions?.addChangeListener { _ -> |
||||
sessionsChanged = true |
||||
// backupIfNecessary() |
||||
} |
||||
|
||||
this.transactions = this.realm.where(Transaction::class.java).findAll() |
||||
this.transactions?.addChangeListener { _ -> |
||||
transactionsChanged = true |
||||
} |
||||
|
||||
} |
||||
|
||||
fun backupIfNecessary() { |
||||
|
||||
Timber.d(">>> backupIfNecessary") |
||||
|
||||
Preferences.getBackupEmail(context)?.let { email -> |
||||
this.backupSessionsIfNecessary(email) |
||||
this.backupTransactionsIfNecessary(email) |
||||
} |
||||
|
||||
} |
||||
|
||||
private fun backupSessionsIfNecessary(email: String) { |
||||
|
||||
if (this.sessionsChanged) { |
||||
Timber.d(">>>> backup sessions") |
||||
val sessions = this.realm.where(Session::class.java).findAll().sort("startDate") |
||||
val csv = ProductCSVDescriptors.pokerAnalyticsAndroid6Sessions.toCSV(sessions) |
||||
val fileName = "sessions_${Date().dateTimeFileFormatted}.csv" |
||||
BackupApi.backupFile(email, fileName, csv) |
||||
this.sessionsChanged = false |
||||
} |
||||
|
||||
} |
||||
|
||||
private fun backupTransactionsIfNecessary(email: String) { |
||||
|
||||
if (this.transactionsChanged) { |
||||
Timber.d(">>>> backup transactions") |
||||
val transactions = this.realm.where(Transaction::class.java).findAll().sort("date") |
||||
val csv = ProductCSVDescriptors.pokerAnalyticsAndroidTransactions.toCSV(transactions) |
||||
val fileName = "transactions_${Date().dateTimeFileFormatted}.csv" |
||||
BackupApi.backupFile(email, fileName, csv) |
||||
this.transactionsChanged = false |
||||
} |
||||
|
||||
} |
||||
|
||||
} |
||||
Loading…
Reference in new issue