Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 33 additions & 11 deletions .github/workflows/build-tester-apk.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build unsigned apk
name: Build apk

on:
workflow_dispatch:
Expand All @@ -7,15 +7,17 @@ on:
description: "Flavor to build"
type: choice
required: true
default: "qa"
default: "original"
options:
- original
- qa
build_type:
description: "Type to build"
type: choice
required: true
default: "Debug" # uppercase for gradlew clean assembleqaDebug
default: "Release" # uppercase for gradlew clean assembleoriginalRelease
options:
- Release
- Debug
version:
description: "Version to build (commit, branch or tag)"
Expand All @@ -27,7 +29,7 @@ permissions:

jobs:
build_apk:
name: Build unsigned APK
name: Build APK
runs-on: ubuntu-latest
outputs:
artifact_name: ${{ steps.set_artifact.outputs.artifact_name }}
Expand All @@ -46,7 +48,7 @@ jobs:
echo "Commit hash: $COMMIT_HASH"
echo "commit_hash=$COMMIT_HASH" >> $GITHUB_OUTPUT
echo "commit_hash=$COMMIT_HASH" >> "$GITHUB_ENV"

- name: Setup JDK
uses: actions/setup-java@v5
with:
Expand All @@ -63,19 +65,35 @@ jobs:
restore-keys: |
${{ runner.os }}-gradle-

- name: Decode keystore
if: inputs.build_type == 'Release'
env:
KEYSTORE_B64: ${{ secrets.KEYSTORE_B64 }}
run: |
if [ -z "$KEYSTORE_B64" ]; then
echo "KEYSTORE_B64 secret is missing - add it under Settings > Secrets > Actions"
exit 1
fi
echo "$KEYSTORE_B64" | base64 -d > "$RUNNER_TEMP/release.jks"

- name: Build APK
run: ./gradlew clean assemble${{ inputs.build_flavor }}${{ inputs.build_type }} -PversionCode=${{ github.run_number }}
env:
OC_RELEASE_KEYSTORE: ${{ inputs.build_type == 'Release' && format('{0}/release.jks', runner.temp) || '' }}
OC_RELEASE_KEYSTORE_PASSWORD: ${{ secrets.KEYSTORE_PASSWORD }}
OC_RELEASE_KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
OC_RELEASE_KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}

- name: Set artifact name
id: set_artifact
run: |
DATE=$(date +%Y%m%d_%H%M)
COMMIT=$(git rev-parse --short HEAD)
FLAVOR="${{ inputs.build_flavor }}"
BUILD_TYPE="debug"
NAME="OpenCloud-${{ inputs.build_flavor }}-${COMMIT}-${DATE}-${GITHUB_RUN_NUMBER}.apk"
BUILD_TYPE=$(echo "${{ inputs.build_type }}" | tr '[:upper:]' '[:lower:]')
NAME="OpenCloud-${FLAVOR}-${BUILD_TYPE}-${COMMIT}-${DATE}-${GITHUB_RUN_NUMBER}.apk"

APK_PATH=$(find ./opencloudApp/build/outputs/apk/$FLAVOR/$BUILD_TYPE/ -name "*.apk")
APK_PATH=$(find ./opencloudApp/build/outputs/apk/$FLAVOR/$BUILD_TYPE/ -name "*.apk" | head -1)

echo "$APK_PATH"
if [ -z "$APK_PATH" ]; then
Expand All @@ -89,6 +107,10 @@ jobs:
# Publish as output
echo "artifact_name=$NAME" >> $GITHUB_OUTPUT

- name: Clean up keystore
if: always()
run: rm -f "$RUNNER_TEMP/release.jks"

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
Expand All @@ -99,10 +121,10 @@ jobs:
- name: Upload APK to release
uses: softprops/action-gh-release@v2
with:
tag_name: build-tester-apk-${{ github.run_number }}
tag_name: build-apk-${{ github.run_number }}
target_commitish: ${{ env.commit_hash }}
name: build-tester-apk build ${{ github.run_number }}
name: ${{ inputs.build_flavor }} ${{ inputs.build_type }} build ${{ github.run_number }}
files: ${{ steps.set_artifact.outputs.artifact_name }}
prerelease: true
prerelease: ${{ inputs.build_type == 'Debug' }}
generate_release_notes: false
body: Built from ${{ env.commit_hash }}
5 changes: 5 additions & 0 deletions opencloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,11 @@
android:name=".presentation.settings.privacypolicy.PrivacyPolicyActivity"
android:label="@string/actionbar_privacy_policy" />
<activity android:name=".presentation.settings.SettingsActivity" />
<activity
android:name=".presentation.weboffice.WebOfficeActivity"
android:configChanges="orientation|screenSize|screenLayout|keyboardHidden|smallestScreenSize"
android:theme="@style/Theme.openCloud"
android:windowSoftInputMode="adjustResize" />
<activity android:name=".presentation.migration.StorageMigrationActivity"
android:theme="@style/Theme.openCloud.Toolbar.Fill" />
<activity
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ package eu.opencloud.android.presentation.files.details
import android.accounts.Account
import android.content.Intent

import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
Expand All @@ -34,7 +33,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import androidx.browser.customtabs.CustomTabsIntent
import eu.opencloud.android.presentation.weboffice.WebOfficeActivity
import androidx.core.view.isVisible
import androidx.work.WorkInfo
import com.google.android.material.snackbar.Snackbar
Expand Down Expand Up @@ -143,11 +142,7 @@ class FileDetailsFragment : FileFragment() {

fileDetailsViewModel.openInWebUriLiveData.observe(viewLifecycleOwner, Event.EventObserver { uiResult: UIResult<String?> ->
if (uiResult is UIResult.Success) {
val builder = CustomTabsIntent.Builder().build()
builder.launchUrl(
requireActivity(),
Uri.parse(uiResult.data)
)
uiResult.data?.let { WebOfficeActivity.open(requireActivity(), it) }
} else if (uiResult is UIResult.Error) {
// Mimetypes not supported via open in web, send 500
if (uiResult.error is InstanceNotConfiguredException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@ import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.drawable.Drawable
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.view.LayoutInflater
Expand All @@ -47,7 +46,6 @@ import androidx.appcompat.app.AlertDialog
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.content.ContextCompat
import androidx.core.content.res.ResourcesCompat
import androidx.core.view.isVisible
Expand Down Expand Up @@ -81,6 +79,7 @@ import eu.opencloud.android.domain.transfers.model.OCTransfer
import eu.opencloud.android.domain.transfers.model.TransferStatus
import eu.opencloud.android.domain.utils.Event
import eu.opencloud.android.extensions.addOpenInWebMenuOptions
import eu.opencloud.android.presentation.weboffice.WebOfficeActivity
import eu.opencloud.android.extensions.collectLatestLifecycleFlow
import eu.opencloud.android.extensions.filterMenuOptions
import eu.opencloud.android.extensions.parseError
Expand Down Expand Up @@ -546,12 +545,8 @@ class MainFileListFragment : Fragment(),
if (it != null) {
val uiResult = it.peekContent()
if (uiResult is UIResult.Success) {
browserOpened = true
val builder = CustomTabsIntent.Builder().build()
builder.launchUrl(
requireActivity(),
Uri.parse(uiResult.data)
)
browserOpened = !WebOfficeActivity.isEditorPayload(uiResult.data)
uiResult.data?.let { WebOfficeActivity.open(requireActivity(), it) }
} else if (uiResult is UIResult.Error) {
// Mimetypes not supported via open in web, send 500
if (uiResult.error is InstanceNotConfiguredException) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
/**
* openCloud Android client application
*
* Shows a web office editor (e.g. Euro-Office) full screen inside the app,
* instead of handing the user off to a browser tab.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 2,
* as published by the Free Software Foundation.
*/

package eu.opencloud.android.presentation.weboffice

import android.annotation.SuppressLint
import android.app.Activity
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.webkit.CookieManager
import android.webkit.ValueCallback
import android.webkit.WebChromeClient
import android.webkit.WebResourceRequest
import android.webkit.WebSettings
import android.webkit.WebView
import android.webkit.WebViewClient
import android.widget.FrameLayout
import android.widget.ProgressBar
import androidx.activity.OnBackPressedCallback
import androidx.appcompat.app.AppCompatActivity
import androidx.browser.customtabs.CustomTabsIntent
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.updatePadding
import org.json.JSONObject
import timber.log.Timber
import java.net.URLEncoder

class WebOfficeActivity : AppCompatActivity() {

private lateinit var webView: WebView
private lateinit var progress: ProgressBar
private var fileChooserCallback: ValueCallback<Array<Uri>>? = null
private var editorHost: String? = null

@SuppressLint("SetJavaScriptEnabled")
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)

val payload = intent.getStringExtra(EXTRA_PAYLOAD)
if (payload == null) {
finish()
return
}

val root = FrameLayout(this)
webView = WebView(this)
progress = ProgressBar(this, null, android.R.attr.progressBarStyleHorizontal).apply {
isIndeterminate = true
}
root.addView(webView, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT))
root.addView(progress, FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.WRAP_CONTENT))
setContentView(root)
supportActionBar?.hide()

// keep the editor out from under the status / navigation bars
ViewCompat.setOnApplyWindowInsetsListener(root) { view, insets ->
val bars = insets.getInsets(WindowInsetsCompat.Type.systemBars() or WindowInsetsCompat.Type.ime())
view.updatePadding(top = bars.top, bottom = bars.bottom, left = bars.left, right = bars.right)
WindowInsetsCompat.CONSUMED
}

with(webView.settings) {
javaScriptEnabled = true
domStorageEnabled = true
databaseEnabled = true
mediaPlaybackRequiresUserGesture = false
cacheMode = WebSettings.LOAD_DEFAULT
setSupportZoom(false)
}
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true)

webView.webViewClient = object : WebViewClient() {
override fun shouldOverrideUrlLoading(view: WebView, request: WebResourceRequest): Boolean {
val url = request.url
// stay inside the editor; anything else (help links, hyperlinks in a doc) goes to a browser tab
return if (editorHost != null && url.host != editorHost && request.isForMainFrame) {
CustomTabsIntent.Builder().build().launchUrl(this@WebOfficeActivity, url)
true
} else {
false
}
}

override fun onPageFinished(view: WebView, url: String) {
progress.visibility = View.GONE
}
}

webView.webChromeClient = object : WebChromeClient() {
// "insert image" etc. in the editor
override fun onShowFileChooser(
view: WebView,
callback: ValueCallback<Array<Uri>>,
params: FileChooserParams
): Boolean {
fileChooserCallback?.onReceiveValue(null)
fileChooserCallback = callback
return try {
@Suppress("DEPRECATION")
startActivityForResult(params.createIntent(), REQUEST_FILE_CHOOSER)
true
} catch (e: Exception) {
fileChooserCallback = null
false
}
}
}

onBackPressedDispatcher.addCallback(this, object : OnBackPressedCallback(true) {
override fun handleOnBackPressed() {
// the editor is a single page app: back closes it (changes autosave server-side)
finish()
}
})

if (savedInstanceState != null) {
webView.restoreState(savedInstanceState)
} else {
loadEditor(payload)
}
}

private fun loadEditor(payload: String) {
try {
val json = JSONObject(payload)
val appUrl = json.getString("app_url")
val method = json.optString("method", "GET").uppercase()
val params = json.optJSONObject("form_parameters")
editorHost = Uri.parse(appUrl).host

val form = buildString {
params?.keys()?.forEach { key ->
if (isNotEmpty()) append('&')
append(URLEncoder.encode(key, "UTF-8")).append('=')
append(URLEncoder.encode(params.getString(key), "UTF-8"))
}
}

if (method == "POST") {
webView.postUrl(appUrl, form.toByteArray())
} else {
val sep = if (appUrl.contains('?')) '&' else '?'
webView.loadUrl(if (form.isEmpty()) appUrl else "$appUrl$sep$form")
}
} catch (e: Exception) {
Timber.e(e, "Could not open web office editor")
finish()
}
}

@Deprecated("Deprecated in Java")
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == REQUEST_FILE_CHOOSER) {
fileChooserCallback?.onReceiveValue(
if (resultCode == Activity.RESULT_OK) WebChromeClient.FileChooserParams.parseResult(resultCode, data) else null
)
fileChooserCallback = null
}
}

override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState)
webView.saveState(outState)
}

override fun onDestroy() {
if (::webView.isInitialized) {
(webView.parent as? ViewGroup)?.removeView(webView)
webView.destroy()
}
super.onDestroy()
}

companion object {
private const val EXTRA_PAYLOAD = "web_office_payload"
private const val REQUEST_FILE_CHOOSER = 4711

/** true if [data] is an /app/open payload (JSON) rather than a plain browser URL */
fun isEditorPayload(data: String?): Boolean = data?.trimStart()?.startsWith("{") == true

/** Opens the editor in-app for a JSON payload, or falls back to a browser tab for a plain URL. */
fun open(context: Context, data: String) {
if (isEditorPayload(data)) {
context.startActivity(Intent(context, WebOfficeActivity::class.java).putExtra(EXTRA_PAYLOAD, data))
} else {
CustomTabsIntent.Builder().build().launchUrl(context, Uri.parse(data))
}
}
}
}
Loading