Skip to content
Open
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
17 changes: 17 additions & 0 deletions opencloudApp/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,23 @@
android:configChanges="orientation|screenSize"
android:theme="@style/Theme.openCloud.Toolbar.Drawer"
android:windowSoftInputMode="adjustPan" />
<!-- Exported trampoline for pinned folder shortcuts: the launcher cannot start the
non-exported FileDisplayActivity directly. Only the shortcut action is accepted
and only the shortcut extras are forwarded. -->
<activity
android:name=".presentation.files.addtohomescreen.ShortcutTrampolineActivity"
android:exported="true"
android:theme="@android:style/Theme.Translucent.NoTitleBar">
<intent-filter>
<action android:name="eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT" />

<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>

<receiver
android:name=".presentation.files.addtohomescreen.ShortcutPinResultReceiver"
android:exported="false" />
<activity
android:name=".ui.activity.ReceiveExternalFilesActivity"
android:configChanges="orientation|screenSize"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import android.content.pm.ResolveInfo
import android.net.Uri
import android.text.method.LinkMovementMethod
import android.util.TypedValue
import android.view.View
import android.view.inputmethod.InputMethodManager
import android.webkit.MimeTypeMap
import android.widget.LinearLayout
Expand Down Expand Up @@ -93,8 +94,14 @@ fun Activity.showMessageInSnackbar(
duration: Int = Snackbar.LENGTH_LONG
) {
// edge-to-edge
val view = if (layoutId == android.R.id.content) window.decorView else findViewById(layoutId)
Snackbar.make(view, message, duration).show()
val view = if (layoutId == android.R.id.content) window.decorView else findViewById<View>(layoutId)
if (view != null) {
Snackbar.make(view, message, duration).show()
} else {
// The requested layout may not be attached yet (e.g. during activity creation).
// Fall back to a Toast, which only needs a Context, so we never crash.
Toast.makeText(this, message, Toast.LENGTH_LONG).show()
}
}

fun Activity.showErrorInToast(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ fun FileMenuOption.toResId() =
FileMenuOption.SEND -> R.id.action_send_file
FileMenuOption.SET_AV_OFFLINE -> R.id.action_set_available_offline
FileMenuOption.UNSET_AV_OFFLINE -> R.id.action_unset_available_offline
FileMenuOption.ADD_TO_HOME_SCREEN -> R.id.action_add_to_home
}

fun FileMenuOption.toStringResId() =
Expand All @@ -59,6 +60,7 @@ fun FileMenuOption.toStringResId() =
FileMenuOption.SEND -> R.string.actionbar_send_file
FileMenuOption.SET_AV_OFFLINE -> R.string.set_available_offline
FileMenuOption.UNSET_AV_OFFLINE -> R.string.unset_available_offline
FileMenuOption.ADD_TO_HOME_SCREEN -> R.string.add_to_home_screen
}

fun FileMenuOption.toDrawableResId() =
Expand All @@ -78,4 +80,5 @@ fun FileMenuOption.toDrawableResId() =
FileMenuOption.SEND -> R.drawable.ic_send_white
FileMenuOption.SET_AV_OFFLINE -> R.drawable.ic_action_set_available_offline
FileMenuOption.UNSET_AV_OFFLINE -> R.drawable.ic_action_unset_available_offline
FileMenuOption.ADD_TO_HOME_SCREEN -> R.drawable.ic_action_add_to_home
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package eu.opencloud.android.presentation.files.addtohomescreen

import android.app.Dialog
import android.os.Bundle
import android.view.WindowManager
import android.widget.EditText
import androidx.appcompat.app.AlertDialog
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.widget.doOnTextChanged
import androidx.fragment.app.DialogFragment
import com.google.android.material.textfield.TextInputLayout
import eu.opencloud.android.R
import eu.opencloud.android.domain.files.model.OCFile
import eu.opencloud.android.presentation.files.filelist.MainFileListFragment.Companion.forbiddenChars
import eu.opencloud.android.utils.PreferenceUtils

class AddToHomeScreenDialogFragment : DialogFragment() {

private lateinit var folder: OCFile
private lateinit var listener: AddToHomeScreenListener

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
folder = requireArguments().getParcelable(ARG_FOLDER)!!
listener = parentFragment as AddToHomeScreenListener
}

override fun onCreateDialog(savedInstanceState: Bundle?): Dialog {
val inflater = requireActivity().layoutInflater
val view = inflater.inflate(R.layout.edit_box_dialog, null)

view.filterTouchesWhenObscured =
PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context)

val coordinatorLayout: CoordinatorLayout = requireActivity().findViewById(R.id.coordinator_layout)
coordinatorLayout.filterTouchesWhenObscured =
PreferenceUtils.shouldDisallowTouchesWithOtherVisibleWindows(context)

val inputText: EditText = view.findViewById(R.id.user_input)
val inputLayout: TextInputLayout = view.findViewById(R.id.edit_box_input_text_layout)

inputText.setText(folder.fileName)
inputText.selectAll()
inputText.requestFocus()

val builder = AlertDialog.Builder(requireActivity())
builder.setView(view)
.setPositiveButton(R.string.add_to_home_screen_dialog_add_button) { dialog, _ ->
val name = inputText.text.toString().trim()
if (name.isNotBlank()) {
listener.onAddToHomeScreen(name, folder)
}
dialog.dismiss()
}
.setNegativeButton(android.R.string.cancel, null)
.setTitle(R.string.add_to_home_screen_dialog_title)

val alertDialog = builder.create()

alertDialog.setOnShowListener {
val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
okButton.isEnabled = inputText.text.isNullOrBlank().not()

okButton.setOnClickListener {
val name = inputText.text.toString().trim()
val error = when {
name.isBlank() -> getString(R.string.add_to_home_screen_dialog_error_empty)
name.length > FolderShortcutHelper.MAX_LONG_LABEL_LENGTH -> String.format(
getString(R.string.add_to_home_screen_dialog_error_length_max),
FolderShortcutHelper.MAX_LONG_LABEL_LENGTH
)
forbiddenChars.any { name.contains(it) } -> getString(R.string.filename_forbidden_characters)
else -> null
}

if (error == null) {
listener.onAddToHomeScreen(name, folder)
alertDialog.dismiss()
} else {
inputLayout.error = error
}
}
}

inputText.doOnTextChanged { text, _, _, _ ->
val okButton = alertDialog.getButton(AlertDialog.BUTTON_POSITIVE)
var error: String? = null

if (text.isNullOrBlank()) {
okButton.isEnabled = false
error = getString(R.string.add_to_home_screen_dialog_error_empty)
} else if (text.length > FolderShortcutHelper.MAX_LONG_LABEL_LENGTH) {
error = String.format(
getString(R.string.add_to_home_screen_dialog_error_length_max),
FolderShortcutHelper.MAX_LONG_LABEL_LENGTH
)
} else if (forbiddenChars.any { text.contains(it) }) {
error = getString(R.string.filename_forbidden_characters)
} else {
okButton.isEnabled = true
}

if (error != null) {
okButton.isEnabled = false
inputLayout.error = error
} else {
inputLayout.error = null
}
}

alertDialog.window?.setSoftInputMode(WindowManager.LayoutParams.SOFT_INPUT_STATE_VISIBLE)
return alertDialog
}

interface AddToHomeScreenListener {
fun onAddToHomeScreen(shortcutName: String, folder: OCFile)
}

companion object {
const val TAG = "ADD_TO_HOME_SCREEN_DIALOG"
private const val ARG_FOLDER = "ARG_FOLDER"

@JvmStatic
fun newInstance(folder: OCFile): AddToHomeScreenDialogFragment =
AddToHomeScreenDialogFragment().apply {
arguments = Bundle().apply {
putParcelable(ARG_FOLDER, folder)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package eu.opencloud.android.presentation.files.addtohomescreen

import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.ShortcutInfo
import android.content.pm.ShortcutManager
import android.graphics.drawable.Icon
import android.os.Build
import android.widget.Toast
import androidx.annotation.RequiresApi
import eu.opencloud.android.R
import eu.opencloud.android.domain.files.model.OCFile

object FolderShortcutHelper {

const val EXTRA_SHORTCUT_FOLDER_REMOTE_ID = "SHORTCUT_FOLDER_REMOTE_ID"
const val EXTRA_SHORTCUT_FOLDER_REMOTE_PATH = "SHORTCUT_FOLDER_REMOTE_PATH"
const val EXTRA_SHORTCUT_FOLDER_SPACE_ID = "SHORTCUT_FOLDER_SPACE_ID"
const val EXTRA_SHORTCUT_FOLDER_ACCOUNT = "SHORTCUT_FOLDER_ACCOUNT"
const val ACTION_OPEN_SHORTCUT =
"eu.opencloud.android.ui.activity.action.OPEN_SHORTCUT"
const val ACTION_PIN_SHORTCUT_RESULT =
"eu.opencloud.android.presentation.files.action.PIN_SHORTCUT_RESULT"

const val MAX_SHORT_LABEL_LENGTH = 10
const val MAX_LONG_LABEL_LENGTH = 25

fun createPinnedShortcut(context: Context, folder: OCFile, shortcutName: String) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createPinnedShortcutApi26(context, folder, shortcutName)
} else {
Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_not_supported), Toast.LENGTH_SHORT).show()
}
}

@RequiresApi(Build.VERSION_CODES.O)
private fun createPinnedShortcutApi26(context: Context, folder: OCFile, shortcutName: String) {
val shortcutManager = context.getSystemService(ShortcutManager::class.java)

if (shortcutManager?.isRequestPinShortcutSupported != true) {
Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_not_supported), Toast.LENGTH_SHORT).show()
return
Comment thread
wernrab marked this conversation as resolved.
}
Comment thread
wernrab marked this conversation as resolved.

val shortcutId = "folder_${folder.id}"

val shortcutIntent = Intent(context, ShortcutTrampolineActivity::class.java).apply {
action = ACTION_OPEN_SHORTCUT
putExtra(EXTRA_SHORTCUT_FOLDER_REMOTE_ID, folder.remoteId)
putExtra(EXTRA_SHORTCUT_FOLDER_REMOTE_PATH, folder.remotePath)
putExtra(EXTRA_SHORTCUT_FOLDER_SPACE_ID, folder.spaceId)
putExtra(EXTRA_SHORTCUT_FOLDER_ACCOUNT, folder.owner)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
}

val shortcut = ShortcutInfo.Builder(context, shortcutId)
.setShortLabel(shortcutName.truncateForLabel(MAX_SHORT_LABEL_LENGTH))
.setLongLabel(shortcutName.truncateForLabel(MAX_LONG_LABEL_LENGTH))
.setIcon(Icon.createWithResource(context, R.mipmap.icon))
.setIntent(shortcutIntent)
Comment on lines +57 to +61
Comment thread
Copilot marked this conversation as resolved.
.build()

val pinResultCallback = PendingIntent.getBroadcast(
context,
0,
Intent(context, ShortcutPinResultReceiver::class.java).setAction(ACTION_PIN_SHORTCUT_RESULT),
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE
)

val requestAccepted = shortcutManager.requestPinShortcut(shortcut, pinResultCallback.intentSender)
if (!requestAccepted) {
Toast.makeText(context, context.getString(R.string.add_to_home_screen_shortcut_failed), Toast.LENGTH_SHORT).show()
}
}

private fun String.truncateForLabel(maxLength: Int): String =
if (length > maxLength) take(maxLength - 1) + "…" else this
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package eu.opencloud.android.presentation.files.addtohomescreen

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.widget.Toast
import eu.opencloud.android.R

class ShortcutPinResultReceiver : BroadcastReceiver() {

override fun onReceive(context: Context, intent: Intent) {
if (intent.action != FolderShortcutHelper.ACTION_PIN_SHORTCUT_RESULT) return
Toast.makeText(
context,
context.getString(R.string.add_to_home_screen_shortcut_added),
Toast.LENGTH_SHORT
).show()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
package eu.opencloud.android.presentation.files.addtohomescreen

import android.app.Activity
import android.content.Intent
import android.os.Bundle
import eu.opencloud.android.ui.activity.FileDisplayActivity

class ShortcutTrampolineActivity : Activity() {

override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
forwardShortcutIntent()
finish()
}

private fun forwardShortcutIntent() {
val shortcutIntent = intent ?: return
if (shortcutIntent.action != FolderShortcutHelper.ACTION_OPEN_SHORTCUT) return

val target = Intent(this, FileDisplayActivity::class.java).apply {
action = FolderShortcutHelper.ACTION_OPEN_SHORTCUT
copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_ID)
copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_REMOTE_PATH)
copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_SPACE_ID)
copyStringExtra(shortcutIntent, FolderShortcutHelper.EXTRA_SHORTCUT_FOLDER_ACCOUNT)
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
}
startActivity(target)
}

private fun Intent.copyStringExtra(source: Intent, key: String) {
source.getStringExtra(key)?.let { putExtra(key, it) }
}
}
Loading
Loading