cd /home/def/outsidethebox/otb-cloud-android || exit 1 set -e BACKUPDIR="/home/def/backuphere/otb-cloud-android" mkdir -p "$BACKUPDIR" mkdir -p /home/def/outsidethebox/apk-drop echo "===== BACKUP =====" cp /home/def/outsidethebox/otb-cloud-android/app/src/main/java/top/outsidethebox/otbcloud/MainActivity.kt \ "$BACKUPDIR/MainActivity.kt.$(date +%Y%m%d-%H%M%S).bak" cp /home/def/outsidethebox/otb-cloud-android/app/build.gradle \ "$BACKUPDIR/app.build.gradle.$(date +%Y%m%d-%H%M%S).bak" cp /home/def/outsidethebox/otb-cloud-android/app/src/main/AndroidManifest.xml \ "$BACKUPDIR/AndroidManifest.xml.$(date +%Y%m%d-%H%M%S).bak" cp /home/def/outsidethebox/otb-cloud-android/app/src/main/res/layout/activity_main.xml \ "$BACKUPDIR/activity_main.xml.$(date +%Y%m%d-%H%M%S).bak" echo "===== PATCH MainActivity.kt =====" python3 - << 'PY' from pathlib import Path import re p = Path("/home/def/outsidethebox/otb-cloud-android/app/src/main/java/top/outsidethebox/otbcloud/MainActivity.kt") text = p.read_text() text = re.sub(r'private const val VIDEO_BATCH_SIZE = \d+', 'private const val VIDEO_BATCH_SIZE = 2', text) text = re.sub(r'private const val IMAGE_BATCH_SIZE = \d+', 'private const val IMAGE_BATCH_SIZE = 25', text) text = re.sub(r'private const val PAUSE_BETWEEN_BATCHES_MS = \d+L', 'private const val PAUSE_BETWEEN_BATCHES_MS = 1500L', text) if 'private const val PAUSE_BETWEEN_FILES_MS' not in text: text = text.replace( 'private const val PAUSE_BETWEEN_BATCHES_MS = 1500L', 'private const val PAUSE_BETWEEN_BATCHES_MS = 1500L\n private const val PAUSE_BETWEEN_FILES_MS = 300L' ) pattern = re.compile( r'private fun uploadSelectedMedia\(selected: List\) \{.*?\n \}', re.S ) replacement = ''' private fun uploadSelectedMedia(selected: List) { val prefs = getPrefs() val deviceUuid = prefs.getString(KEY_DEVICE_UUID, null) if (deviceUuid.isNullOrBlank()) { binding.statusText.text = "Missing local device UUID" Toast.makeText(this, "Missing device UUID", Toast.LENGTH_LONG).show() return } binding.uploadSelectedButton.isEnabled = false binding.scanButton.isEnabled = false binding.runScanButton.isEnabled = false binding.selectAllButton.isEnabled = false binding.clearAllButton.isEnabled = false executor.execute { var uploaded = 0 var skipped = 0 var failed = 0 val containsVideo = selected.any { it.mimeType.startsWith("video/") } val batchSize = if (containsVideo) VIDEO_BATCH_SIZE else IMAGE_BATCH_SIZE val batches = selected.chunked(batchSize) for ((batchIndex, batch) in batches.withIndex()) { runOnUiThread { binding.statusText.text = "Starting batch ${batchIndex + 1} of ${batches.size} (${batch.size} file(s))" } for ((itemIndex, item) in batch.withIndex()) { runOnUiThread { binding.statusText.text = "Batch ${batchIndex + 1}/${batches.size} - File ${itemIndex + 1}/${batch.size}: ${item.displayName}" } if (serverFileExists(deviceUuid, item)) { skipped += 1 runOnUiThread { binding.statusText.text = "Batch ${batchIndex + 1}/${batches.size} - Skipping existing: ${item.displayName}" } } else { val ok = uploadSingleFile(deviceUuid, item) if (ok) { uploaded += 1 } else { failed += 1 } } try { Thread.sleep(PAUSE_BETWEEN_FILES_MS) } catch (_: Exception) { } System.gc() } if (batchIndex < batches.lastIndex) { runOnUiThread { binding.statusText.text = "Completed batch ${batchIndex + 1}/${batches.size}. Pausing before next batch..." } try { Thread.sleep(PAUSE_BETWEEN_BATCHES_MS) } catch (_: Exception) { } System.gc() } } runOnUiThread { binding.uploadSelectedButton.isEnabled = true binding.scanButton.isEnabled = true binding.runScanButton.isEnabled = true binding.selectAllButton.isEnabled = true binding.clearAllButton.isEnabled = true binding.statusText.text = "Upload complete: $uploaded uploaded, $skipped skipped, $failed failed" Toast.makeText( this, "Upload complete: $uploaded uploaded, $skipped skipped, $failed failed", Toast.LENGTH_LONG ).show() } } }''' new_text, count = pattern.subn(replacement, text, count=1) if count != 1: raise SystemExit("FAILED: could not replace uploadSelectedMedia function") p.write_text(new_text) print("MainActivity.kt updated") PY echo "===== PATCH VERSION FILES TO v0.2.2 =====" python3 - << 'PY' from pathlib import Path import re files = [ Path("/home/def/outsidethebox/otb-cloud-android/app/build.gradle"), Path("/home/def/outsidethebox/otb-cloud-android/app/src/main/AndroidManifest.xml"), Path("/home/def/outsidethebox/otb-cloud-android/app/src/main/res/layout/activity_main.xml"), ] for p in files: text = p.read_text() text = re.sub(r'versionCode\s+\d+', 'versionCode 22', text) text = re.sub(r'versionName\s+"[^"]+"', 'versionName "0.2.2"', text) text = text.replace('OTB Cloud v0.2.1', 'OTB Cloud v0.2.2') text = text.replace('OTB Cloud v0.2.0', 'OTB Cloud v0.2.2') text = text.replace('OTB Cloud v0.1.9', 'OTB Cloud v0.2.2') text = text.replace('Android Backup Client v0.2.1', 'Android Backup Client v0.2.2') text = text.replace('Android Backup Client v0.2.0', 'Android Backup Client v0.2.2') text = text.replace('Android Backup Client v0.1.9', 'Android Backup Client v0.2.2') p.write_text(text) print("Version files updated") PY echo "===== VERIFY SOURCE =====" grep -n 'VIDEO_BATCH_SIZE\|IMAGE_BATCH_SIZE\|PAUSE_BETWEEN_BATCHES_MS\|PAUSE_BETWEEN_FILES_MS' \ /home/def/outsidethebox/otb-cloud-android/app/src/main/java/top/outsidethebox/otbcloud/MainActivity.kt grep -nE 'versionCode|versionName' /home/def/outsidethebox/otb-cloud-android/app/build.gradle grep -n 'android:label=' /home/def/outsidethebox/otb-cloud-android/app/src/main/AndroidManifest.xml echo "===== BUILD =====" cd /home/def/outsidethebox/otb-cloud-android || exit 1 ./gradlew clean assembleDebug --no-daemon echo "===== COPY APK =====" cp /home/def/outsidethebox/otb-cloud-android/app/build/outputs/apk/debug/app-debug.apk \ /home/def/outsidethebox/apk-drop/otb-cloud-v0.2.2.apk echo "===== VERIFY APK =====" aapt dump badging /home/def/outsidethebox/otb-cloud-android/app/build/outputs/apk/debug/app-debug.apk 2>/dev/null | grep version || true echo "===== FINAL APK =====" ls -lh /home/def/outsidethebox/apk-drop/otb-cloud-v0.2.2.apk