Merge remote-tracking branch 'origin/master' into dev

This commit is contained in:
Tobias Kaminsky 2024-07-31 02:31:10 +02:00
commit 9a3062d7ff
21 changed files with 509 additions and 63 deletions

View file

@ -159,7 +159,7 @@ class FileDownloadWorker(
} }
private fun setIdleWorkerState() { private fun setIdleWorkerState() {
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle) WorkerStateLiveData.instance().setWorkState(WorkerState.Idle(getCurrentFile()))
} }
private fun removePendingDownload(accountName: String?) { private fun removePendingDownload(accountName: String?) {

View file

@ -64,6 +64,9 @@ class FileUploadHelper {
companion object { companion object {
private val TAG = FileUploadWorker::class.java.simpleName private val TAG = FileUploadWorker::class.java.simpleName
@Suppress("MagicNumber")
const val MAX_FILE_COUNT = 500
val mBoundListeners = HashMap<String, OnDatatransferProgressListener>() val mBoundListeners = HashMap<String, OnDatatransferProgressListener>()
private var instance: FileUploadHelper? = null private var instance: FileUploadHelper? = null

View file

@ -124,7 +124,7 @@ class FileUploadWorker(
} }
private fun setIdleWorkerState() { private fun setIdleWorkerState() {
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle) WorkerStateLiveData.instance().setWorkState(WorkerState.Idle(currentUploadFileOperation?.file))
} }
@Suppress("ReturnCount") @Suppress("ReturnCount")

View file

@ -0,0 +1,30 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.model
import com.owncloud.android.R
enum class SearchResultEntryType {
CalendarEvent,
Folder,
Note,
Contact,
Deck,
Unknown;
fun iconId(): Int {
return when (this) {
Folder -> R.drawable.folder
Note -> R.drawable.ic_edit
Contact -> R.drawable.file_vcard
CalendarEvent -> R.drawable.file_calendar
Deck -> R.drawable.ic_deck
else -> R.drawable.ic_find_in_page
}
}
}

View file

@ -8,11 +8,12 @@
package com.nextcloud.model package com.nextcloud.model
import com.nextcloud.client.account.User import com.nextcloud.client.account.User
import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.db.OCUpload import com.owncloud.android.db.OCUpload
import com.owncloud.android.operations.DownloadFileOperation import com.owncloud.android.operations.DownloadFileOperation
sealed class WorkerState { sealed class WorkerState {
object Idle : WorkerState() data class Idle(var currentFile: OCFile?) : WorkerState()
class Download(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState() data class Download(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
class Upload(var user: User?, var uploads: List<OCUpload>) : WorkerState() data class Upload(var user: User?, var uploads: List<OCUpload>) : WorkerState()
} }

View file

@ -0,0 +1,78 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils
import android.Manifest
import android.content.ContentUris
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.CalendarContract
import com.nextcloud.utils.extensions.showToast
import com.owncloud.android.R
import com.owncloud.android.lib.common.SearchResultEntry
import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
import com.owncloud.android.utils.PermissionUtil.checkSelfPermission
class CalendarEventManager(private val context: Context) {
fun openCalendarEvent(searchResult: SearchResultEntry, listInterface: UnifiedSearchListInterface) {
val havePermission = checkSelfPermission(context, Manifest.permission.READ_CALENDAR)
val createdAt = searchResult.createdAt()
val eventId: Long? = if (havePermission && createdAt != null) {
getCalendarEventId(searchResult.title, createdAt)
} else {
null
}
if (eventId == null) {
val messageId = if (havePermission) {
R.string.unified_search_fragment_calendar_event_not_found
} else {
R.string.unified_search_fragment_permission_needed
}
context.showToast(messageId)
listInterface.onSearchResultClicked(searchResult)
} else {
val uri: Uri = ContentUris.withAppendedId(CalendarContract.Events.CONTENT_URI, eventId)
val intent = Intent(Intent.ACTION_VIEW).setData(uri)
context.startActivity(intent)
}
}
private fun getCalendarEventId(eventTitle: String, eventStartDate: Long): Long? {
val projection = arrayOf(
CalendarContract.Events._ID,
CalendarContract.Events.TITLE,
CalendarContract.Events.DTSTART
)
val selection = "${CalendarContract.Events.TITLE} = ? AND ${CalendarContract.Events.DTSTART} = ?"
val selectionArgs = arrayOf(eventTitle, eventStartDate.toString())
val cursor = context.contentResolver.query(
CalendarContract.Events.CONTENT_URI,
projection,
selection,
selectionArgs,
"${CalendarContract.Events.DTSTART} ASC"
)
cursor?.use {
if (cursor.moveToFirst()) {
val idIndex = cursor.getColumnIndex(CalendarContract.Events._ID)
return cursor.getLong(idIndex)
}
}
return null
}
}
@Suppress("MagicNumber")
private fun SearchResultEntry.createdAt(): Long? = attributes["createdAt"]?.toLongOrNull()?.times(1000L)

View file

@ -0,0 +1,144 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils
import android.Manifest
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.provider.ContactsContract
import com.nextcloud.utils.extensions.showToast
import com.owncloud.android.R
import com.owncloud.android.lib.common.SearchResultEntry
import com.owncloud.android.ui.interfaces.UnifiedSearchListInterface
import com.owncloud.android.utils.PermissionUtil.checkSelfPermission
class ContactManager(private val context: Context) {
fun openContact(searchResult: SearchResultEntry, listInterface: UnifiedSearchListInterface) {
val havePermission = checkSelfPermission(context, Manifest.permission.READ_CONTACTS)
val displayName = searchResult.displayName()
val contactId: Long? = if (havePermission && displayName != null) {
getContactIds(displayName).let { contactIds ->
if (contactIds.size > 1) getContactId(searchResult, contactIds) else contactIds.firstOrNull()
}
} else {
null
}
if (contactId == null) {
val messageId = if (havePermission) {
R.string.unified_search_fragment_contact_not_found
} else {
R.string.unified_search_fragment_permission_needed
}
context.showToast(messageId)
listInterface.onSearchResultClicked(searchResult)
} else {
val uri = Uri.withAppendedPath(ContactsContract.Contacts.CONTENT_URI, contactId.toString())
val intent = Intent(Intent.ACTION_VIEW).apply {
setData(uri)
}
context.startActivity(intent)
}
}
private fun getContactId(searchResult: SearchResultEntry, contactIds: List<Long>): Long? {
val email = searchResult.email()
val phoneNumber = searchResult.phoneNumber()
contactIds.forEach {
val targetEmail = getEmailById(it) ?: ""
val targetPhoneNumber = getPhoneNumberById(it) ?: ""
if (targetEmail == email && targetPhoneNumber == phoneNumber) {
return it
}
}
return null
}
private fun getEmailById(contactId: Long): String? {
var result: String? = null
val projection = arrayOf(ContactsContract.CommonDataKinds.Email.ADDRESS)
val selection = "${ContactsContract.CommonDataKinds.Email.CONTACT_ID} = ?"
val selectionArgs = arrayOf(contactId.toString())
val cursor = context.contentResolver.query(
ContactsContract.CommonDataKinds.Email.CONTENT_URI,
projection,
selection,
selectionArgs,
null
)
cursor?.use {
val emailIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Email.ADDRESS)
while (cursor.moveToNext()) {
result = cursor.getString(emailIndex)
}
}
return result
}
private fun getPhoneNumberById(contactId: Long): String? {
var result: String? = null
val projection = arrayOf(ContactsContract.CommonDataKinds.Phone.NUMBER)
val selection = "${ContactsContract.CommonDataKinds.Phone.CONTACT_ID} = ?"
val selectionArgs = arrayOf(contactId.toString())
val cursor = context.contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
selection,
selectionArgs,
null
)
cursor?.use {
val phoneIndex = cursor.getColumnIndex(ContactsContract.CommonDataKinds.Phone.NUMBER)
while (cursor.moveToNext()) {
result = cursor.getString(phoneIndex)
}
}
return result
}
private fun getContactIds(displayName: String): List<Long> {
val result = arrayListOf<Long>()
val projection = arrayOf(ContactsContract.Contacts._ID)
val selection = "${ContactsContract.Contacts.DISPLAY_NAME} = ?"
val selectionArgs = arrayOf(displayName)
val cursor = context.contentResolver.query(
ContactsContract.Contacts.CONTENT_URI,
projection,
selection,
selectionArgs,
null
)
cursor?.use {
val idIndex = cursor.getColumnIndex(ContactsContract.Contacts._ID)
while (cursor.moveToNext()) {
val id = cursor.getLong(idIndex)
result.add(id)
}
}
return result
}
}
private fun SearchResultEntry.displayName(): String? = attributes["displayName"]
private fun SearchResultEntry.email(): String? = attributes["email"]
private fun SearchResultEntry.phoneNumber(): String? = attributes["phoneNumber"]

View file

@ -0,0 +1,27 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.nextcloud.utils.extensions
import com.nextcloud.model.SearchResultEntryType
import com.owncloud.android.lib.common.SearchResultEntry
fun SearchResultEntry.getType(): SearchResultEntryType {
return if (icon == "icon-folder") {
SearchResultEntryType.Folder
} else if (icon.startsWith("icon-note")) {
SearchResultEntryType.Note
} else if (icon.startsWith("icon-contacts")) {
SearchResultEntryType.Contact
} else if (icon.startsWith("icon-calendar")) {
SearchResultEntryType.CalendarEvent
} else if (icon.startsWith("icon-deck")) {
SearchResultEntryType.Deck
} else {
SearchResultEntryType.Unknown
}
}

View file

@ -880,7 +880,7 @@ public class ReceiveExternalFilesActivity extends FileActivity
} }
private boolean somethingToUpload() { private boolean somethingToUpload() {
return (mStreamsToUpload != null && mStreamsToUpload.size() > 0 && mStreamsToUpload.get(0) != null || return (mStreamsToUpload != null && !mStreamsToUpload.isEmpty() && mStreamsToUpload.get(0) != null ||
mUploadFromTmpFile); mUploadFromTmpFile);
} }
@ -904,6 +904,11 @@ public class ReceiveExternalFilesActivity extends FileActivity
return; return;
} }
if (mStreamsToUpload.size() > FileUploadHelper.MAX_FILE_COUNT) {
DisplayUtils.showSnackMessage(this, R.string.max_file_count_warning_message);
return;
}
UriUploader uploader = new UriUploader( UriUploader uploader = new UriUploader(
this, this,
mStreamsToUpload, mStreamsToUpload,

View file

@ -30,6 +30,7 @@ import android.widget.TextView;
import com.nextcloud.client.account.User; import com.nextcloud.client.account.User;
import com.nextcloud.client.di.Injectable; import com.nextcloud.client.di.Injectable;
import com.nextcloud.client.jobs.upload.FileUploadHelper;
import com.nextcloud.client.jobs.upload.FileUploadWorker; import com.nextcloud.client.jobs.upload.FileUploadWorker;
import com.nextcloud.client.preferences.AppPreferences; import com.nextcloud.client.preferences.AppPreferences;
import com.nextcloud.utils.extensions.ActivityExtensionsKt; import com.nextcloud.utils.extensions.ActivityExtensionsKt;
@ -653,6 +654,11 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
@Override @Override
public void onConfirmation(String callerTag) { public void onConfirmation(String callerTag) {
Log_OC.d(TAG, "Positive button in dialog was clicked; dialog tag is " + callerTag); Log_OC.d(TAG, "Positive button in dialog was clicked; dialog tag is " + callerTag);
if (mFileListFragment.getCheckedFilePaths().length > FileUploadHelper.MAX_FILE_COUNT) {
DisplayUtils.showSnackMessage(this, R.string.max_file_count_warning_message);
return;
}
if (QUERY_TO_MOVE_DIALOG_TAG.equals(callerTag)) { if (QUERY_TO_MOVE_DIALOG_TAG.equals(callerTag)) {
// return the list of selected files to the caller activity (success), // return the list of selected files to the caller activity (success),
// signaling that they should be moved to the ownCloud folder, instead of copied // signaling that they should be moved to the ownCloud folder, instead of copied

View file

@ -16,9 +16,13 @@ import com.afollestad.sectionedrecyclerview.SectionedViewHolder
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import com.bumptech.glide.request.RequestListener import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.target.Target import com.bumptech.glide.request.target.Target
import com.nextcloud.android.common.ui.theme.utils.ColorRole
import com.nextcloud.client.account.User import com.nextcloud.client.account.User
import com.nextcloud.client.network.ClientFactory import com.nextcloud.client.network.ClientFactory
import com.owncloud.android.R import com.nextcloud.model.SearchResultEntryType
import com.nextcloud.utils.CalendarEventManager
import com.nextcloud.utils.ContactManager
import com.nextcloud.utils.extensions.getType
import com.owncloud.android.databinding.UnifiedSearchItemBinding import com.owncloud.android.databinding.UnifiedSearchItemBinding
import com.owncloud.android.datamodel.FileDataStorageManager import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.lib.common.SearchResultEntry import com.owncloud.android.lib.common.SearchResultEntry
@ -30,6 +34,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
@Suppress("LongParameterList") @Suppress("LongParameterList")
class UnifiedSearchItemViewHolder( class UnifiedSearchItemViewHolder(
private val supportsOpeningCalendarContactsLocally: Boolean,
val binding: UnifiedSearchItemBinding, val binding: UnifiedSearchItemBinding,
val user: User, val user: User,
val clientFactory: ClientFactory, val clientFactory: ClientFactory,
@ -38,13 +43,15 @@ class UnifiedSearchItemViewHolder(
private val filesAction: FilesAction, private val filesAction: FilesAction,
val context: Context, val context: Context,
private val viewThemeUtils: ViewThemeUtils private val viewThemeUtils: ViewThemeUtils
) : ) : SectionedViewHolder(binding.root) {
SectionedViewHolder(binding.root) {
interface FilesAction { interface FilesAction {
fun showFilesAction(searchResultEntry: SearchResultEntry) fun showFilesAction(searchResultEntry: SearchResultEntry)
} }
private val contactManager = ContactManager(context)
private val calendarEventManager = CalendarEventManager(context)
fun bind(entry: SearchResultEntry) { fun bind(entry: SearchResultEntry) {
binding.title.text = entry.title binding.title.text = entry.title
binding.subline.text = entry.subline binding.subline.text = entry.subline
@ -57,7 +64,8 @@ class UnifiedSearchItemViewHolder(
val mimetype = MimeTypeUtil.getBestMimeTypeByFilename(entry.title) val mimetype = MimeTypeUtil.getBestMimeTypeByFilename(entry.title)
val placeholder = getPlaceholder(entry, mimetype) val entryType = entry.getType()
val placeholder = getPlaceholder(entry, entryType, mimetype)
Glide.with(context).using(CustomGlideStreamLoader(user, clientFactory)) Glide.with(context).using(CustomGlideStreamLoader(user, clientFactory))
.load(entry.thumbnailUrl) .load(entry.thumbnailUrl)
@ -70,32 +78,50 @@ class UnifiedSearchItemViewHolder(
if (entry.isFile) { if (entry.isFile) {
binding.more.visibility = View.VISIBLE binding.more.visibility = View.VISIBLE
binding.more.setOnClickListener { filesAction.showFilesAction(entry) } binding.more.setOnClickListener {
filesAction.showFilesAction(entry)
}
} else { } else {
binding.more.visibility = View.GONE binding.more.visibility = View.GONE
} }
binding.unifiedSearchItemLayout.setOnClickListener { listInterface.onSearchResultClicked(entry) } binding.unifiedSearchItemLayout.setOnClickListener {
searchEntryOnClick(entry, entryType)
}
} }
private fun getPlaceholder(entry: SearchResultEntry, mimetype: String?): Drawable { private fun searchEntryOnClick(entry: SearchResultEntry, entryType: SearchResultEntryType) {
val drawable = with(entry.icon) { if (supportsOpeningCalendarContactsLocally) {
when { when (entryType) {
equals("icon-folder") -> SearchResultEntryType.Contact -> {
ResourcesCompat.getDrawable(context.resources, R.drawable.folder, null) contactManager.openContact(entry, listInterface)
startsWith("icon-note") -> }
ResourcesCompat.getDrawable(context.resources, R.drawable.ic_edit, null)
startsWith("icon-contacts") -> SearchResultEntryType.CalendarEvent -> {
ResourcesCompat.getDrawable(context.resources, R.drawable.file_vcard, null) calendarEventManager.openCalendarEvent(entry, listInterface)
startsWith("icon-calendar") -> }
ResourcesCompat.getDrawable(context.resources, R.drawable.file_calendar, null)
startsWith("icon-deck") -> else -> {
ResourcesCompat.getDrawable(context.resources, R.drawable.ic_deck, null) listInterface.onSearchResultClicked(entry)
else ->
MimeTypeUtil.getFileTypeIcon(mimetype, entry.title, context, viewThemeUtils)
} }
} }
return viewThemeUtils.platform.tintPrimaryDrawable(context, drawable)!! } else {
listInterface.onSearchResultClicked(entry)
}
}
private fun getPlaceholder(
entry: SearchResultEntry,
entryType: SearchResultEntryType,
mimetype: String?
): Drawable {
val iconId = entryType.run {
iconId()
}
val defaultDrawable = MimeTypeUtil.getFileTypeIcon(mimetype, entry.title, context, viewThemeUtils)
val drawable: Drawable = ResourcesCompat.getDrawable(context.resources, iconId, null) ?: defaultDrawable
return viewThemeUtils.platform.tintDrawable(context, drawable, ColorRole.PRIMARY)
} }
private inner class RoundIfNeededListener(private val entry: SearchResultEntry) : private inner class RoundIfNeededListener(private val entry: SearchResultEntry) :

View file

@ -33,6 +33,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
*/ */
@Suppress("LongParameterList") @Suppress("LongParameterList")
class UnifiedSearchListAdapter( class UnifiedSearchListAdapter(
private val supportsOpeningCalendarContactsLocally: Boolean,
private val storageManager: FileDataStorageManager, private val storageManager: FileDataStorageManager,
private val listInterface: UnifiedSearchListInterface, private val listInterface: UnifiedSearchListInterface,
private val filesAction: UnifiedSearchItemViewHolder.FilesAction, private val filesAction: UnifiedSearchItemViewHolder.FilesAction,
@ -73,6 +74,7 @@ class UnifiedSearchListAdapter(
false false
) )
UnifiedSearchItemViewHolder( UnifiedSearchItemViewHolder(
supportsOpeningCalendarContactsLocally,
binding, binding,
user, user,
clientFactory, clientFactory,

View file

@ -7,6 +7,7 @@
*/ */
package com.owncloud.android.ui.fragment package com.owncloud.android.ui.fragment
import android.Manifest
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
@ -16,6 +17,7 @@ import android.view.MenuItem
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.core.view.updatePadding import androidx.core.view.updatePadding
@ -23,6 +25,7 @@ import androidx.fragment.app.Fragment
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
import androidx.recyclerview.widget.GridLayoutManager import androidx.recyclerview.widget.GridLayoutManager
import com.nextcloud.client.account.CurrentAccountProvider import com.nextcloud.client.account.CurrentAccountProvider
import com.nextcloud.client.account.UserAccountManager
import com.nextcloud.client.core.AsyncRunner import com.nextcloud.client.core.AsyncRunner
import com.nextcloud.client.di.Injectable import com.nextcloud.client.di.Injectable
import com.nextcloud.client.di.ViewModelFactory import com.nextcloud.client.di.ViewModelFactory
@ -33,6 +36,7 @@ import com.owncloud.android.datamodel.FileDataStorageManager
import com.owncloud.android.datamodel.OCFile import com.owncloud.android.datamodel.OCFile
import com.owncloud.android.lib.common.SearchResultEntry import com.owncloud.android.lib.common.SearchResultEntry
import com.owncloud.android.lib.common.utils.Log_OC import com.owncloud.android.lib.common.utils.Log_OC
import com.owncloud.android.lib.resources.status.NextcloudVersion
import com.owncloud.android.ui.activity.FileDisplayActivity import com.owncloud.android.ui.activity.FileDisplayActivity
import com.owncloud.android.ui.adapter.UnifiedSearchItemViewHolder import com.owncloud.android.ui.adapter.UnifiedSearchItemViewHolder
import com.owncloud.android.ui.adapter.UnifiedSearchListAdapter import com.owncloud.android.ui.adapter.UnifiedSearchListAdapter
@ -44,6 +48,7 @@ import com.owncloud.android.ui.unifiedsearch.UnifiedSearchSection
import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel import com.owncloud.android.ui.unifiedsearch.UnifiedSearchViewModel
import com.owncloud.android.ui.unifiedsearch.filterOutHiddenFiles import com.owncloud.android.ui.unifiedsearch.filterOutHiddenFiles
import com.owncloud.android.utils.DisplayUtils import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.PermissionUtil
import com.owncloud.android.utils.theme.ViewThemeUtils import com.owncloud.android.utils.theme.ViewThemeUtils
import javax.inject.Inject import javax.inject.Inject
@ -97,8 +102,10 @@ class UnifiedSearchFragment :
@Inject @Inject
lateinit var viewThemeUtils: ViewThemeUtils lateinit var viewThemeUtils: ViewThemeUtils
private var listOfHiddenFiles = ArrayList<String>() @Inject
lateinit var accountManager: UserAccountManager
private var listOfHiddenFiles = ArrayList<String>()
private var showMoreActions = false private var showMoreActions = false
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
@ -132,7 +139,15 @@ class UnifiedSearchFragment :
setupFileDisplayActivity() setupFileDisplayActivity()
setupAdapter() setupAdapter()
if (supportsOpeningCalendarContactsLocally()) {
checkPermissions()
} }
}
private fun supportsOpeningCalendarContactsLocally(): Boolean = storageManager
.getCapability(accountManager.user)
.version
.isNewerOrEqual(NextcloudVersion.nextcloud_30)
@Deprecated("Deprecated in Java") @Deprecated("Deprecated in Java")
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -140,6 +155,21 @@ class UnifiedSearchFragment :
setupSearchView(item) setupSearchView(item)
} }
private fun checkPermissions() {
val permissions = arrayOf(Manifest.permission.READ_CONTACTS, Manifest.permission.READ_CALENDAR)
if (!PermissionUtil.checkPermissions(requireContext(), permissions)) {
permissionLauncher.launch(permissions)
}
}
private val permissionLauncher =
registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { permissions ->
val granted = permissions.entries.all { it.value }
if (!granted) {
DisplayUtils.showSnackMessage(binding.root, R.string.unified_search_fragment_permission_needed)
}
}
private fun setupSearchView(item: MenuItem) { private fun setupSearchView(item: MenuItem) {
(item.actionView as? SearchView?)?.run { (item.actionView as? SearchView?)?.run {
// Required to align with TextView width. // Required to align with TextView width.
@ -230,6 +260,7 @@ class UnifiedSearchFragment :
private fun setupAdapter() { private fun setupAdapter() {
val gridLayoutManager = GridLayoutManager(requireContext(), 1) val gridLayoutManager = GridLayoutManager(requireContext(), 1)
adapter = UnifiedSearchListAdapter( adapter = UnifiedSearchListAdapter(
supportsOpeningCalendarContactsLocally(),
storageManager, storageManager,
this, this,
this, this,

View file

@ -11,7 +11,6 @@ import com.owncloud.android.lib.common.SearchResultEntry
import com.owncloud.android.ui.unifiedsearch.ProviderID import com.owncloud.android.ui.unifiedsearch.ProviderID
interface UnifiedSearchListInterface { interface UnifiedSearchListInterface {
fun onSearchResultClicked(searchResultEntry: SearchResultEntry) fun onSearchResultClicked(searchResultEntry: SearchResultEntry)
fun onLoadMoreClicked(providerID: ProviderID) fun onLoadMoreClicked(providerID: ProviderID)
} }

View file

@ -1,15 +1,8 @@
/* /*
* Nextcloud - Android Client * Nextcloud - Android Client
* *
* SPDX-FileCopyrightText: 2020-2024 Andy Scherzinger <info@andy-scherzinger.de> * SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com> * SPDX-License-Identifier: AGPL-3.0-or-later
* SPDX-FileCopyrightText: 2022 Álvaro Brey <alvaro@alvarobrey.com>
* SPDX-FileCopyrightText: 2019 Tobias Kaminsky <tobias@kaminsky.me>
* SPDX-FileCopyrightText: 2019 Chris Narkiewicz <hello@ezaquarii.com>
* SPDX-FileCopyrightText: 2016 ownCloud Inc.
* SPDX-FileCopyrightText: 2015 María Asensio Valverde <masensio@solidgear.es>
* SPDX-FileCopyrightText: 2013 David A. Velasco <dvelasco@solidgear.es>
* SPDX-License-Identifier: GPL-2.0-only AND (AGPL-3.0-or-later OR GPL-2.0-only)
*/ */
package com.owncloud.android.ui.preview package com.owncloud.android.ui.preview
@ -56,6 +49,8 @@ import com.owncloud.android.ui.activity.FileDisplayActivity
import com.owncloud.android.ui.fragment.FileFragment import com.owncloud.android.ui.fragment.FileFragment
import com.owncloud.android.ui.fragment.GalleryFragment import com.owncloud.android.ui.fragment.GalleryFragment
import com.owncloud.android.ui.fragment.OCFileListFragment import com.owncloud.android.ui.fragment.OCFileListFragment
import com.owncloud.android.ui.preview.model.PreviewImageActivityState
import com.owncloud.android.utils.DisplayUtils
import com.owncloud.android.utils.MimeTypeUtil import com.owncloud.android.utils.MimeTypeUtil
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings import edu.umd.cs.findbugs.annotations.SuppressFBWarnings
import java.io.Serializable import java.io.Serializable
@ -65,16 +60,18 @@ import kotlin.math.max
/** /**
* Holds a swiping gallery where image files contained in an Nextcloud directory are shown. * Holds a swiping gallery where image files contained in an Nextcloud directory are shown.
*/ */
@Suppress("TooManyFunctions")
class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnRemoteOperationListener, Injectable { class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnRemoteOperationListener, Injectable {
private var livePhotoFile: OCFile? = null private var livePhotoFile: OCFile? = null
private var viewPager: ViewPager2? = null private var viewPager: ViewPager2? = null
private var previewImagePagerAdapter: PreviewImagePagerAdapter? = null private var previewImagePagerAdapter: PreviewImagePagerAdapter? = null
private var savedPosition = 0 private var savedPosition = 0
private var hasSavedPosition = false private var hasSavedPosition = false
private var requestWaitingForBinder = false
private var downloadFinishReceiver: DownloadFinishReceiver? = null private var downloadFinishReceiver: DownloadFinishReceiver? = null
private var fullScreenAnchorView: View? = null private var fullScreenAnchorView: View? = null
private var isDownloadWorkStarted = false private var isDownloadWorkStarted = false
private var screenState = PreviewImageActivityState.Idle
@Inject @Inject
lateinit var preferences: AppPreferences lateinit var preferences: AppPreferences
@ -115,7 +112,10 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
// to keep our UI controls visibility in line with system bars visibility // to keep our UI controls visibility in line with system bars visibility
setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED) setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED)
requestWaitingForBinder = savedInstanceState?.getBoolean(KEY_WAITING_FOR_BINDER) ?: false val requestWaitingForBinder = savedInstanceState?.getBoolean(KEY_WAITING_FOR_BINDER) ?: false
if (requestWaitingForBinder) {
screenState = PreviewImageActivityState.WaitingForBinder
}
observeWorkerState() observeWorkerState()
} }
@ -181,7 +181,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
if (position == 0 && !file.isDown) { if (position == 0 && !file.isDown) {
// this is necessary because mViewPager.setCurrentItem(0) just after setting the // this is necessary because mViewPager.setCurrentItem(0) just after setting the
// adapter does not result in a call to #onPageSelected(0) // adapter does not result in a call to #onPageSelected(0)
requestWaitingForBinder = true screenState = PreviewImageActivityState.WaitingForBinder
} }
} }
@ -241,7 +241,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
outState.putBoolean(KEY_WAITING_FOR_BINDER, requestWaitingForBinder) outState.putBoolean(KEY_WAITING_FOR_BINDER, screenState == PreviewImageActivityState.WaitingForBinder)
outState.putBoolean(KEY_SYSTEM_VISIBLE, isSystemUIVisible) outState.putBoolean(KEY_SYSTEM_VISIBLE, isSystemUIVisible)
} }
@ -254,11 +254,15 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
previewImagePagerAdapter?.let { previewImagePagerAdapter?.let {
if (it.itemCount <= 1) { if (it.itemCount <= 1) {
finish() backToDisplayActivity()
return return
} }
} }
if (user.isPresent) {
initViewPager(user.get())
}
viewPager?.setCurrentItem(nextPosition, true) viewPager?.setCurrentItem(nextPosition, true)
previewImagePagerAdapter?.delete(deletePosition) previewImagePagerAdapter?.delete(deletePosition)
} else if (operation is SynchronizeFileOperation) { } else if (operation is SynchronizeFileOperation) {
@ -274,12 +278,35 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
private fun observeWorkerState() { private fun observeWorkerState() {
WorkerStateLiveData.instance().observe(this) { state: WorkerState? -> WorkerStateLiveData.instance().observe(this) { state: WorkerState? ->
if (state is WorkerState.Download) { when (state) {
is WorkerState.Download -> {
Log_OC.d(TAG, "Download worker started") Log_OC.d(TAG, "Download worker started")
isDownloadWorkStarted = true isDownloadWorkStarted = true
if (requestWaitingForBinder) { if (screenState == PreviewImageActivityState.WaitingForBinder) {
requestWaitingForBinder = false selectPageOnDownload()
}
}
is WorkerState.Idle -> {
Log_OC.d(TAG, "Download worker stopped")
isDownloadWorkStarted = false
if (screenState == PreviewImageActivityState.Edit) {
onImageDownloadComplete(state.currentFile)
}
}
else -> {
Log_OC.d(TAG, "Download worker stopped")
isDownloadWorkStarted = false
}
}
}
}
private fun selectPageOnDownload() {
screenState = PreviewImageActivityState.Idle
Log_OC.d( Log_OC.d(
TAG, TAG,
"Simulating reselection of current page after connection " + "Simulating reselection of current page after connection " +
@ -287,11 +314,12 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
) )
selectPage(viewPager?.currentItem) selectPage(viewPager?.currentItem)
} }
} else {
Log_OC.d(TAG, "Download worker stopped") private fun onImageDownloadComplete(downloadedFile: OCFile?) {
isDownloadWorkStarted = false dismissLoadingDialog()
} screenState = PreviewImageActivityState.Idle
} file = downloadedFile
startEditImageActivity()
} }
override fun onResume() { override fun onResume() {
@ -316,6 +344,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
} }
private fun backToDisplayActivity() { private fun backToDisplayActivity() {
sendRefreshSearchEventBroadcast()
finish() finish()
} }
@ -328,7 +357,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
} }
startActivity(intent) startActivity(intent)
finish() backToDisplayActivity()
} }
override fun showDetails(file: OCFile, activeTab: Int) { override fun showDetails(file: OCFile, activeTab: Int) {
@ -356,7 +385,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
val currentFile = previewImagePagerAdapter?.getFileAt(position) val currentFile = previewImagePagerAdapter?.getFileAt(position)
if (!isDownloadWorkStarted) { if (!isDownloadWorkStarted) {
requestWaitingForBinder = true screenState = PreviewImageActivityState.WaitingForBinder
} else { } else {
if (currentFile != null) { if (currentFile != null) {
if (currentFile.isEncrypted && !currentFile.isDown && if (currentFile.isEncrypted && !currentFile.isDown &&
@ -457,14 +486,31 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
fun startImageEditor(file: OCFile) { fun startImageEditor(file: OCFile) {
if (file.isDown) { if (file.isDown) {
val editImageIntent = Intent(this, EditImageActivity::class.java) startEditImageActivity()
editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file)
startActivity(editImageIntent)
} else { } else {
showLoadingDialog(getString(R.string.preview_image_downloading_image_for_edit))
screenState = PreviewImageActivityState.Edit
requestForDownload(file, EditImageActivity.OPEN_IMAGE_EDITOR) requestForDownload(file, EditImageActivity.OPEN_IMAGE_EDITOR)
} }
} }
private fun startEditImageActivity() {
if (file == null) {
DisplayUtils.showSnackMessage(this, R.string.preview_image_file_is_not_exist)
return
}
if (!file.isDown) {
DisplayUtils.showSnackMessage(this, R.string.preview_image_file_is_not_downloaded)
return
}
val intent = Intent(this, EditImageActivity::class.java).apply {
putExtra(EditImageActivity.EXTRA_FILE, file)
}
startActivity(intent)
}
override fun onBrowsedDownTo(folder: OCFile) { override fun onBrowsedDownTo(folder: OCFile) {
// TODO Auto-generated method stub // TODO Auto-generated method stub
} }

View file

@ -0,0 +1,14 @@
/*
* Nextcloud - Android Client
*
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
package com.owncloud.android.ui.preview.model
enum class PreviewImageActivityState {
WaitingForBinder,
Edit,
Idle
}

View file

@ -95,6 +95,10 @@ object PermissionUtil {
else -> checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE) else -> checkSelfPermission(context, Manifest.permission.WRITE_EXTERNAL_STORAGE)
} }
fun checkPermissions(context: Context, permissions: Array<String>): Boolean = permissions.all {
ActivityCompat.checkSelfPermission(context, it) == PackageManager.PERMISSION_GRANTED
}
/** /**
* Request relevant external storage permission depending on SDK, if needed. * Request relevant external storage permission depending on SDK, if needed.
* *

View file

@ -0,0 +1,19 @@
<!--
~ Nextcloud - Android Client
~
~ SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
~ SPDX-License-Identifier: AGPL-3.0-or-later
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:tint="#FF969696"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M20,19.59V8l-6,-6H6c-1.1,0 -1.99,0.9 -1.99,2L4,20c0,1.1 0.89,2 1.99,2H18c0.45,0 0.85,-0.15 1.19,-0.4l-4.43,-4.43c-0.8,0.52 -1.74,0.83 -2.76,0.83 -2.76,0 -5,-2.24 -5,-5s2.24,-5 5,-5 5,2.24 5,5c0,1.02 -0.31,1.96 -0.83,2.75L20,19.59zM9,13c0,1.66 1.34,3 3,3s3,-1.34 3,-3 -1.34,-3 -3,-3 -3,1.34 -3,3z" />
</vector>

View file

@ -841,6 +841,7 @@ GNU yleinen lisenssi, versio 2</string>
<string name="upload_local_storage_full">Paikallinen tallennustila täynnä</string> <string name="upload_local_storage_full">Paikallinen tallennustila täynnä</string>
<string name="upload_local_storage_not_copied">Tiedostoa ei voitu kopioida paikalliseen tallennustilaan</string> <string name="upload_local_storage_not_copied">Tiedostoa ei voitu kopioida paikalliseen tallennustilaan</string>
<string name="upload_lock_failed">Kansion lukitseminen epäonnistui</string> <string name="upload_lock_failed">Kansion lukitseminen epäonnistui</string>
<string name="upload_manually_cancelled">Käyttäjä peruutti latauksen</string>
<string name="upload_old_android">Salaus on mahdollista vain kun &gt;= Android 5.0</string> <string name="upload_old_android">Salaus on mahdollista vain kun &gt;= Android 5.0</string>
<string name="upload_query_move_foreign_files">Riittämätön tila estää tiedostojen kopioinnin %1$s kansioon. Haluatko sen sijaan siirtää ne sinne?</string> <string name="upload_query_move_foreign_files">Riittämätön tila estää tiedostojen kopioinnin %1$s kansioon. Haluatko sen sijaan siirtää ne sinne?</string>
<string name="upload_scan_doc_upload">Skannaa asiakirja kameralla</string> <string name="upload_scan_doc_upload">Skannaa asiakirja kameralla</string>

View file

@ -677,6 +677,7 @@
<string name="push_notifications_temp_error">Atualmente o envio de notificações está indisponível.</string> <string name="push_notifications_temp_error">Atualmente o envio de notificações está indisponível.</string>
<string name="qr_could_not_be_read">Não foi possível ler o código QR! </string> <string name="qr_could_not_be_read">Não foi possível ler o código QR! </string>
<string name="receive_external_files_activity_start_sync_folder_is_not_exists_message">A pasta não pode ser encontrada, a operação de sincronização foi cancelada</string> <string name="receive_external_files_activity_start_sync_folder_is_not_exists_message">A pasta não pode ser encontrada, a operação de sincronização foi cancelada</string>
<string name="receive_external_files_activity_unable_to_find_file_to_upload">Não foi possível encontrar o arquivo para upload</string>
<string name="recommend_subject">Experimente %1$s em seu dispositivo!</string> <string name="recommend_subject">Experimente %1$s em seu dispositivo!</string>
<string name="recommend_text">Quero convidar você a usar %1$s em seu dispositivo.\nBaixe daqui: %2$s</string> <string name="recommend_text">Quero convidar você a usar %1$s em seu dispositivo.\nBaixe daqui: %2$s</string>
<string name="recommend_urls">%1$s ou %2$s</string> <string name="recommend_urls">%1$s ou %2$s</string>

View file

@ -129,6 +129,7 @@
<string name="uploader_error_message_source_file_not_found">File selected for upload not found. Please check whether the file exists.</string> <string name="uploader_error_message_source_file_not_found">File selected for upload not found. Please check whether the file exists.</string>
<string name="uploader_error_message_source_file_not_copied">Could not copy file to a temporary folder. Try to resend it.</string> <string name="uploader_error_message_source_file_not_copied">Could not copy file to a temporary folder. Try to resend it.</string>
<string name="uploader_upload_files_behaviour">Upload option:</string> <string name="uploader_upload_files_behaviour">Upload option:</string>
<string name="max_file_count_warning_message">You have reached the maximum file upload limit. Please upload fewer than 500 files at a time.</string>
<string name="uploader_upload_files_behaviour_move_to_nextcloud_folder">Move file to %1$s folder</string> <string name="uploader_upload_files_behaviour_move_to_nextcloud_folder">Move file to %1$s folder</string>
<string name="uploader_upload_files_behaviour_only_upload">Keep file in source folder</string> <string name="uploader_upload_files_behaviour_only_upload">Keep file in source folder</string>
<string name="uploader_upload_files_behaviour_upload_and_delete_from_source">Delete file from source folder</string> <string name="uploader_upload_files_behaviour_upload_and_delete_from_source">Delete file from source folder</string>
@ -419,6 +420,9 @@
<string name="preview_media_unhandled_http_code_message">File is currently locked by another user or process and therefore not deletable. Please try again later.</string> <string name="preview_media_unhandled_http_code_message">File is currently locked by another user or process and therefore not deletable. Please try again later.</string>
<string name="preview_sorry">Sorry</string> <string name="preview_sorry">Sorry</string>
<string name="preview_image_file_is_not_exist">File is not exist</string>
<string name="preview_image_file_is_not_downloaded">File is not downloaded</string>
<string name="preview_image_downloading_image_for_edit">Downloading image to start the edit screen, please wait…</string>
<string name="preview_image_description">Image preview</string> <string name="preview_image_description">Image preview</string>
<string name="preview_image_error_unknown_format">Unable to show image</string> <string name="preview_image_error_unknown_format">Unable to show image</string>
<string name="preview_image_error_no_local_file">There is no local file to preview</string> <string name="preview_image_error_no_local_file">There is no local file to preview</string>
@ -1209,4 +1213,9 @@
<string name="sub_folder_rule_day">Year/Month/Day</string> <string name="sub_folder_rule_day">Year/Month/Day</string>
<string name="secure_share_not_set_up">Secure sharing is not set up for this user</string> <string name="secure_share_not_set_up">Secure sharing is not set up for this user</string>
<string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string> <string name="share_not_allowed_when_file_drop">Resharing is not allowed during secure file drop</string>
<string name="unified_search_fragment_calendar_event_not_found">Event not found, you can always sync to update. Redirecting to web…</string>
<string name="unified_search_fragment_contact_not_found">Contact not found, you can always sync to update. Redirecting to web…</string>
<string name="unified_search_fragment_permission_needed">Permissions are required to open search result otherwise it will redirected to web…</string>
</resources> </resources>