Skip to content

Commit d30ac8d

Browse files
committed
Adds Android sample app for FastPix Uploads SDK
1 parent c0e4c89 commit d30ac8d

11 files changed

Lines changed: 532 additions & 1 deletion

File tree

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,4 +8,3 @@
88
.externalNativeBuild
99
.cxx
1010
local.properties
11-
app

app/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/build

app/build.gradle.kts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
plugins {
2+
alias(libs.plugins.android.application)
3+
alias(libs.plugins.kotlin.android)
4+
}
5+
6+
android {
7+
namespace = "io.fastpix.uploads.sample"
8+
compileSdk = 35
9+
10+
defaultConfig {
11+
applicationId = "io.fastpix.uploads.sample"
12+
minSdk = 24
13+
targetSdk = 35
14+
versionCode = 1
15+
versionName = "1.0"
16+
17+
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
18+
}
19+
20+
buildTypes {
21+
release {
22+
isMinifyEnabled = false
23+
proguardFiles(
24+
getDefaultProguardFile("proguard-android-optimize.txt"),
25+
"proguard-rules.pro"
26+
)
27+
}
28+
}
29+
30+
compileOptions {
31+
sourceCompatibility = JavaVersion.VERSION_11
32+
targetCompatibility = JavaVersion.VERSION_11
33+
}
34+
35+
kotlinOptions {
36+
jvmTarget = "11"
37+
}
38+
39+
buildFeatures {
40+
viewBinding = true
41+
}
42+
}
43+
44+
dependencies {
45+
implementation(project(":uploader"))
46+
47+
implementation(libs.androidx.core.ktx)
48+
implementation(libs.androidx.appcompat)
49+
implementation(libs.material)
50+
implementation(libs.androidx.activity)
51+
implementation(libs.androidx.constraintlayout)
52+
53+
testImplementation(libs.junit)
54+
androidTestImplementation(libs.androidx.junit)
55+
androidTestImplementation(libs.androidx.espresso.core)
56+
}

app/proguard-rules.pro

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
# Add project specific ProGuard rules here.

app/src/main/AndroidManifest.xml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<?xml version="1.0" encoding="utf-8"?>
2+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
3+
4+
<uses-permission android:name="android.permission.INTERNET" />
5+
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
6+
7+
<application
8+
android:allowBackup="true"
9+
android:label="@string/app_name"
10+
android:supportsRtl="true"
11+
android:theme="@style/Theme.FastPixUploadsSample">
12+
13+
<activity
14+
android:name=".MainActivity"
15+
android:exported="true">
16+
<intent-filter>
17+
<action android:name="android.intent.action.MAIN" />
18+
<category android:name="android.intent.category.LAUNCHER" />
19+
</intent-filter>
20+
</activity>
21+
22+
</application>
23+
24+
</manifest>
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
package io.fastpix.uploads.sample
2+
3+
import android.net.Uri
4+
import android.os.Bundle
5+
import android.provider.OpenableColumns
6+
import android.util.Base64
7+
import android.util.Log
8+
import android.widget.Toast
9+
import androidx.activity.result.contract.ActivityResultContracts
10+
import androidx.appcompat.app.AppCompatActivity
11+
import io.fastpix.uploads.FastPixUploader
12+
import io.fastpix.uploads.UploadError
13+
import io.fastpix.uploads.UploadListener
14+
import io.fastpix.uploads.UploadState
15+
import io.fastpix.uploads.sample.databinding.ActivityMainBinding
16+
import okhttp3.Call
17+
import okhttp3.Callback
18+
import okhttp3.MediaType.Companion.toMediaType
19+
import okhttp3.OkHttpClient
20+
import okhttp3.Request
21+
import okhttp3.RequestBody
22+
import okhttp3.RequestBody.Companion.toRequestBody
23+
import okhttp3.Response
24+
import org.json.JSONObject
25+
import java.io.File
26+
import java.io.FileOutputStream
27+
import java.io.IOException
28+
import kotlin.math.roundToInt
29+
30+
class MainActivity : AppCompatActivity() {
31+
32+
private lateinit var binding: ActivityMainBinding
33+
34+
private var selectedFile: File? = null
35+
private var uploader: FastPixUploader? = null
36+
37+
private val pickFile =
38+
registerForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
39+
if (uri != null) handlePickedFile(uri)
40+
}
41+
42+
override fun onCreate(savedInstanceState: Bundle?) {
43+
super.onCreate(savedInstanceState)
44+
binding = ActivityMainBinding.inflate(layoutInflater)
45+
setContentView(binding.root)
46+
supportActionBar?.hide()
47+
binding.pickFileButton.setOnClickListener { pickFile.launch(arrayOf("*/*")) }
48+
binding.startUploadButton.setOnClickListener { getSignedUrl() }
49+
binding.pauseButton.setOnClickListener { uploader?.pause() }
50+
binding.resumeButton.setOnClickListener { uploader?.resume() }
51+
binding.abortButton.setOnClickListener { uploader?.cancel() }
52+
}
53+
54+
private fun handlePickedFile(uri: Uri) {
55+
val displayName = queryDisplayName(uri) ?: "upload_${System.currentTimeMillis()}"
56+
val destination = File(cacheDir, displayName)
57+
58+
runCatching {
59+
contentResolver.openInputStream(uri)?.use { input ->
60+
FileOutputStream(destination).use { output -> input.copyTo(output) }
61+
} ?: error("Unable to open input stream")
62+
}.onSuccess {
63+
selectedFile = destination
64+
binding.selectedFileText.text = "${destination.name} (${destination.length()} bytes)"
65+
}.onFailure {
66+
selectedFile = null
67+
Toast.makeText(this, R.string.error_copy_failed, Toast.LENGTH_LONG).show()
68+
}
69+
}
70+
71+
private fun queryDisplayName(uri: Uri): String? {
72+
contentResolver.query(uri, null, null, null, null)?.use { cursor ->
73+
val nameIndex = cursor.getColumnIndex(OpenableColumns.DISPLAY_NAME)
74+
if (nameIndex >= 0 && cursor.moveToFirst()) return cursor.getString(nameIndex)
75+
}
76+
return null
77+
}
78+
79+
private fun getRequestForSignedUrl(): RequestBody {
80+
val mediaType = "application/json; charset=utf-8".toMediaType()
81+
val request =
82+
"{\"corsOrigin\":\"*\",\"pushMediaSettings\":{\"metadata\":{\"key1\":\"value1\"},\"accessPolicy\":\"public\",\"maxResolution\":\"2160p\"}}".toRequestBody(
83+
mediaType
84+
)
85+
return request
86+
}
87+
88+
private var _signedUrl: String? = null
89+
private var _uploadId: String? = null
90+
91+
private var _token = ""
92+
private var _secretKey = ""
93+
94+
private fun getSignedUrl() {
95+
val requestBody = getRequestForSignedUrl()
96+
val credentials = _token.plus(":").plus(_secretKey)
97+
val auth = "Basic ".plus(Base64.encodeToString(credentials.toByteArray(), Base64.NO_WRAP))
98+
val headers = mapOf(Pair("Authorization", auth), Pair("Content-Type", "application/json"))
99+
OkHttpHelper.post(
100+
"https://api.fastpix.com/v1/on-demand/upload",
101+
headers = headers,
102+
body = requestBody,
103+
callback = object : Callback {
104+
override fun onFailure(call: Call, e: IOException) {
105+
Log.e("TAG", "onFailure: ${e.printStackTrace()}", )
106+
}
107+
108+
override fun onResponse(call: Call, response: Response) {
109+
if (response.isSuccessful) {
110+
val responseBody = response.body?.string()
111+
val jsonObject = JSONObject(responseBody)
112+
val jsonData = jsonObject.getJSONObject("data")
113+
_signedUrl = jsonData.getString("url")
114+
_uploadId = jsonData.getString("uploadId")
115+
// OkHttp invokes this callback on its dispatcher thread.
116+
// startUpload() touches Views, so hop back to the main looper.
117+
runOnUiThread { startUpload(_signedUrl) }
118+
}
119+
}
120+
121+
})
122+
}
123+
124+
private fun startUpload(signedUrl: String?) {
125+
val file = selectedFile
126+
if (file == null) {
127+
Toast.makeText(this, R.string.error_file_required, Toast.LENGTH_SHORT).show()
128+
return
129+
}
130+
131+
runCatching {
132+
FastPixUploader.Builder(this)
133+
.file(file)
134+
.sessionUri(signedUrl!!)
135+
.chunkSize(8L * 1024 * 1024) // 8 MiB (multiple of 256 KiB)
136+
.maxRetries(5)
137+
.retryBaseDelay(2_000L)
138+
.retryMaxDelay(30_000L)
139+
.listener(uploadListener)
140+
.build()
141+
}.onSuccess { sdk ->
142+
uploader = sdk
143+
setUploadingUiState(true)
144+
sdk.start()
145+
}.onFailure { error ->
146+
Toast.makeText(
147+
this,
148+
error.message ?: error::class.java.simpleName,
149+
Toast.LENGTH_LONG,
150+
).show()
151+
}
152+
}
153+
154+
private fun setUploadingUiState(uploading: Boolean) {
155+
binding.startUploadButton.isEnabled = !uploading
156+
binding.pickFileButton.isEnabled = !uploading
157+
binding.pauseButton.isEnabled = uploading
158+
binding.abortButton.isEnabled = uploading
159+
}
160+
161+
private fun updateStatus(text: String) {
162+
binding.statusText.text = "Status: $text"
163+
}
164+
165+
// Listener callbacks already arrive on the main looper thanks to CallbackDispatcher,
166+
// so no runOnUiThread wrappers are needed here.
167+
private val uploadListener = object : UploadListener {
168+
override fun onStateChange(state: UploadState) {
169+
updateStatus("state: $state")
170+
if (state.isTerminal) setUploadingUiState(false)
171+
}
172+
173+
override fun onProgress(bytesUploaded: Long, totalBytes: Long, percentage: Double) {
174+
val pct = percentage.roundToInt().coerceIn(0, 100)
175+
binding.uploadProgress.progress = pct
176+
binding.progressText.text = "$pct%"
177+
}
178+
179+
override fun onPrepared(totalChunks: Int, totalBytes: Long, chunkSize: Long) {
180+
updateStatus("prepared: $totalChunks chunks, $totalBytes bytes")
181+
}
182+
183+
override fun onChunkUploaded(chunkIndex: Int, totalChunks: Int, bytesAcked: Long) {
184+
updateStatus("chunk $chunkIndex / $totalChunks")
185+
}
186+
187+
override fun onRetryScheduled(attempt: Int, delayMillis: Long, cause: UploadError) {
188+
updateStatus("retry #$attempt in ${delayMillis} ms (${cause.message})")
189+
}
190+
191+
override fun onNetworkStateChange(online: Boolean) {
192+
updateStatus(if (online) "online" else "offline")
193+
}
194+
195+
override fun onSuccess(elapsedMillis: Long) {
196+
binding.uploadProgress.progress = 100
197+
binding.progressText.text = "100%"
198+
updateStatus("completed in $elapsedMillis ms")
199+
}
200+
201+
override fun onFailure(error: UploadError, elapsedMillis: Long) {
202+
updateStatus("failure: ${error.message}")
203+
}
204+
205+
override fun onCancelled(elapsedMillis: Long) {
206+
updateStatus("cancelled after $elapsedMillis ms")
207+
}
208+
}
209+
210+
override fun onDestroy() {
211+
uploader?.cancel()
212+
uploader = null
213+
super.onDestroy()
214+
}
215+
}
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package io.fastpix.uploads.sample
2+
3+
4+
5+
import okhttp3.Callback
6+
import okhttp3.MediaType.Companion.toMediaTypeOrNull
7+
import okhttp3.OkHttpClient
8+
import okhttp3.Request
9+
import okhttp3.RequestBody
10+
import okhttp3.RequestBody.Companion.toRequestBody
11+
import okhttp3.logging.HttpLoggingInterceptor
12+
import java.util.concurrent.TimeUnit
13+
14+
object OkHttpHelper {
15+
16+
private val client: OkHttpClient by lazy {
17+
val interceptor = HttpLoggingInterceptor()
18+
interceptor.level = HttpLoggingInterceptor.Level.BODY
19+
val builder = OkHttpClient.Builder()
20+
builder.addInterceptor(interceptor)
21+
builder.build()
22+
OkHttpClient.Builder()
23+
.addInterceptor(interceptor)
24+
.readTimeout(10000, TimeUnit.SECONDS)
25+
.writeTimeout(10000, TimeUnit.SECONDS)
26+
.connectTimeout(10000, TimeUnit.SECONDS)
27+
.build()
28+
}
29+
30+
fun post(
31+
url: String,
32+
headers: Map<String, String> = emptyMap(),
33+
body: RequestBody,
34+
callback: Callback
35+
) {
36+
val request = Request.Builder()
37+
.url(url)
38+
.post(body)
39+
.apply {
40+
headers.forEach { (key, value) ->
41+
addHeader(key, value)
42+
}
43+
}
44+
.build()
45+
46+
client.newCall(request).enqueue(callback)
47+
}
48+
49+
fun put(
50+
url: String,
51+
fileContent: ByteArray,
52+
callback: Callback
53+
) {
54+
val requestBody =
55+
fileContent.toRequestBody(
56+
"application/octet-stream".toMediaTypeOrNull(),
57+
)
58+
59+
val request = Request.Builder()
60+
.url(url)
61+
.put(requestBody) // Use .post() for POST requests
62+
.build();
63+
client.newCall(request).enqueue(callback)
64+
}
65+
}

0 commit comments

Comments
 (0)