mirror of
https://github.com/nextcloud/android.git
synced 2024-12-23 01:00:26 +03:00
Merge remote-tracking branch 'origin/master' into dev
This commit is contained in:
commit
9a3062d7ff
21 changed files with 509 additions and 63 deletions
|
@ -159,7 +159,7 @@ class FileDownloadWorker(
|
|||
}
|
||||
|
||||
private fun setIdleWorkerState() {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle(getCurrentFile()))
|
||||
}
|
||||
|
||||
private fun removePendingDownload(accountName: String?) {
|
||||
|
|
|
@ -64,6 +64,9 @@ class FileUploadHelper {
|
|||
companion object {
|
||||
private val TAG = FileUploadWorker::class.java.simpleName
|
||||
|
||||
@Suppress("MagicNumber")
|
||||
const val MAX_FILE_COUNT = 500
|
||||
|
||||
val mBoundListeners = HashMap<String, OnDatatransferProgressListener>()
|
||||
|
||||
private var instance: FileUploadHelper? = null
|
||||
|
|
|
@ -124,7 +124,7 @@ class FileUploadWorker(
|
|||
}
|
||||
|
||||
private fun setIdleWorkerState() {
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle)
|
||||
WorkerStateLiveData.instance().setWorkState(WorkerState.Idle(currentUploadFileOperation?.file))
|
||||
}
|
||||
|
||||
@Suppress("ReturnCount")
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -8,11 +8,12 @@
|
|||
package com.nextcloud.model
|
||||
|
||||
import com.nextcloud.client.account.User
|
||||
import com.owncloud.android.datamodel.OCFile
|
||||
import com.owncloud.android.db.OCUpload
|
||||
import com.owncloud.android.operations.DownloadFileOperation
|
||||
|
||||
sealed class WorkerState {
|
||||
object Idle : WorkerState()
|
||||
class Download(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
|
||||
class Upload(var user: User?, var uploads: List<OCUpload>) : WorkerState()
|
||||
data class Idle(var currentFile: OCFile?) : WorkerState()
|
||||
data class Download(var user: User?, var currentDownload: DownloadFileOperation?) : WorkerState()
|
||||
data class Upload(var user: User?, var uploads: List<OCUpload>) : WorkerState()
|
||||
}
|
||||
|
|
|
@ -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)
|
144
app/src/main/java/com/nextcloud/utils/ContactManager.kt
Normal file
144
app/src/main/java/com/nextcloud/utils/ContactManager.kt
Normal 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"]
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -880,7 +880,7 @@ public class ReceiveExternalFilesActivity extends FileActivity
|
|||
}
|
||||
|
||||
private boolean somethingToUpload() {
|
||||
return (mStreamsToUpload != null && mStreamsToUpload.size() > 0 && mStreamsToUpload.get(0) != null ||
|
||||
return (mStreamsToUpload != null && !mStreamsToUpload.isEmpty() && mStreamsToUpload.get(0) != null ||
|
||||
mUploadFromTmpFile);
|
||||
}
|
||||
|
||||
|
@ -904,6 +904,11 @@ public class ReceiveExternalFilesActivity extends FileActivity
|
|||
return;
|
||||
}
|
||||
|
||||
if (mStreamsToUpload.size() > FileUploadHelper.MAX_FILE_COUNT) {
|
||||
DisplayUtils.showSnackMessage(this, R.string.max_file_count_warning_message);
|
||||
return;
|
||||
}
|
||||
|
||||
UriUploader uploader = new UriUploader(
|
||||
this,
|
||||
mStreamsToUpload,
|
||||
|
|
|
@ -30,6 +30,7 @@ import android.widget.TextView;
|
|||
|
||||
import com.nextcloud.client.account.User;
|
||||
import com.nextcloud.client.di.Injectable;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadHelper;
|
||||
import com.nextcloud.client.jobs.upload.FileUploadWorker;
|
||||
import com.nextcloud.client.preferences.AppPreferences;
|
||||
import com.nextcloud.utils.extensions.ActivityExtensionsKt;
|
||||
|
@ -653,6 +654,11 @@ public class UploadFilesActivity extends DrawerActivity implements LocalFileList
|
|||
@Override
|
||||
public void onConfirmation(String 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)) {
|
||||
// return the list of selected files to the caller activity (success),
|
||||
// signaling that they should be moved to the ownCloud folder, instead of copied
|
||||
|
|
|
@ -16,9 +16,13 @@ import com.afollestad.sectionedrecyclerview.SectionedViewHolder
|
|||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestListener
|
||||
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.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.datamodel.FileDataStorageManager
|
||||
import com.owncloud.android.lib.common.SearchResultEntry
|
||||
|
@ -30,6 +34,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
|
|||
|
||||
@Suppress("LongParameterList")
|
||||
class UnifiedSearchItemViewHolder(
|
||||
private val supportsOpeningCalendarContactsLocally: Boolean,
|
||||
val binding: UnifiedSearchItemBinding,
|
||||
val user: User,
|
||||
val clientFactory: ClientFactory,
|
||||
|
@ -38,13 +43,15 @@ class UnifiedSearchItemViewHolder(
|
|||
private val filesAction: FilesAction,
|
||||
val context: Context,
|
||||
private val viewThemeUtils: ViewThemeUtils
|
||||
) :
|
||||
SectionedViewHolder(binding.root) {
|
||||
) : SectionedViewHolder(binding.root) {
|
||||
|
||||
interface FilesAction {
|
||||
fun showFilesAction(searchResultEntry: SearchResultEntry)
|
||||
}
|
||||
|
||||
private val contactManager = ContactManager(context)
|
||||
private val calendarEventManager = CalendarEventManager(context)
|
||||
|
||||
fun bind(entry: SearchResultEntry) {
|
||||
binding.title.text = entry.title
|
||||
binding.subline.text = entry.subline
|
||||
|
@ -57,7 +64,8 @@ class UnifiedSearchItemViewHolder(
|
|||
|
||||
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))
|
||||
.load(entry.thumbnailUrl)
|
||||
|
@ -70,32 +78,50 @@ class UnifiedSearchItemViewHolder(
|
|||
|
||||
if (entry.isFile) {
|
||||
binding.more.visibility = View.VISIBLE
|
||||
binding.more.setOnClickListener { filesAction.showFilesAction(entry) }
|
||||
binding.more.setOnClickListener {
|
||||
filesAction.showFilesAction(entry)
|
||||
}
|
||||
} else {
|
||||
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 {
|
||||
val drawable = with(entry.icon) {
|
||||
when {
|
||||
equals("icon-folder") ->
|
||||
ResourcesCompat.getDrawable(context.resources, R.drawable.folder, null)
|
||||
startsWith("icon-note") ->
|
||||
ResourcesCompat.getDrawable(context.resources, R.drawable.ic_edit, null)
|
||||
startsWith("icon-contacts") ->
|
||||
ResourcesCompat.getDrawable(context.resources, R.drawable.file_vcard, null)
|
||||
startsWith("icon-calendar") ->
|
||||
ResourcesCompat.getDrawable(context.resources, R.drawable.file_calendar, null)
|
||||
startsWith("icon-deck") ->
|
||||
ResourcesCompat.getDrawable(context.resources, R.drawable.ic_deck, null)
|
||||
else ->
|
||||
MimeTypeUtil.getFileTypeIcon(mimetype, entry.title, context, viewThemeUtils)
|
||||
private fun searchEntryOnClick(entry: SearchResultEntry, entryType: SearchResultEntryType) {
|
||||
if (supportsOpeningCalendarContactsLocally) {
|
||||
when (entryType) {
|
||||
SearchResultEntryType.Contact -> {
|
||||
contactManager.openContact(entry, listInterface)
|
||||
}
|
||||
|
||||
SearchResultEntryType.CalendarEvent -> {
|
||||
calendarEventManager.openCalendarEvent(entry, listInterface)
|
||||
}
|
||||
|
||||
else -> {
|
||||
listInterface.onSearchResultClicked(entry)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
listInterface.onSearchResultClicked(entry)
|
||||
}
|
||||
return viewThemeUtils.platform.tintPrimaryDrawable(context, drawable)!!
|
||||
}
|
||||
|
||||
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) :
|
||||
|
|
|
@ -33,6 +33,7 @@ import com.owncloud.android.utils.theme.ViewThemeUtils
|
|||
*/
|
||||
@Suppress("LongParameterList")
|
||||
class UnifiedSearchListAdapter(
|
||||
private val supportsOpeningCalendarContactsLocally: Boolean,
|
||||
private val storageManager: FileDataStorageManager,
|
||||
private val listInterface: UnifiedSearchListInterface,
|
||||
private val filesAction: UnifiedSearchItemViewHolder.FilesAction,
|
||||
|
@ -73,6 +74,7 @@ class UnifiedSearchListAdapter(
|
|||
false
|
||||
)
|
||||
UnifiedSearchItemViewHolder(
|
||||
supportsOpeningCalendarContactsLocally,
|
||||
binding,
|
||||
user,
|
||||
clientFactory,
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
*/
|
||||
package com.owncloud.android.ui.fragment
|
||||
|
||||
import android.Manifest
|
||||
import android.content.Intent
|
||||
import android.os.Bundle
|
||||
import android.view.LayoutInflater
|
||||
|
@ -16,6 +17,7 @@ import android.view.MenuItem
|
|||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ImageView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import androidx.appcompat.widget.SearchView
|
||||
import androidx.core.view.updatePadding
|
||||
|
@ -23,6 +25,7 @@ import androidx.fragment.app.Fragment
|
|||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.recyclerview.widget.GridLayoutManager
|
||||
import com.nextcloud.client.account.CurrentAccountProvider
|
||||
import com.nextcloud.client.account.UserAccountManager
|
||||
import com.nextcloud.client.core.AsyncRunner
|
||||
import com.nextcloud.client.di.Injectable
|
||||
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.lib.common.SearchResultEntry
|
||||
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.adapter.UnifiedSearchItemViewHolder
|
||||
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.filterOutHiddenFiles
|
||||
import com.owncloud.android.utils.DisplayUtils
|
||||
import com.owncloud.android.utils.PermissionUtil
|
||||
import com.owncloud.android.utils.theme.ViewThemeUtils
|
||||
import javax.inject.Inject
|
||||
|
||||
|
@ -97,8 +102,10 @@ class UnifiedSearchFragment :
|
|||
@Inject
|
||||
lateinit var viewThemeUtils: ViewThemeUtils
|
||||
|
||||
private var listOfHiddenFiles = ArrayList<String>()
|
||||
@Inject
|
||||
lateinit var accountManager: UserAccountManager
|
||||
|
||||
private var listOfHiddenFiles = ArrayList<String>()
|
||||
private var showMoreActions = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
|
@ -132,14 +139,37 @@ class UnifiedSearchFragment :
|
|||
|
||||
setupFileDisplayActivity()
|
||||
setupAdapter()
|
||||
if (supportsOpeningCalendarContactsLocally()) {
|
||||
checkPermissions()
|
||||
}
|
||||
}
|
||||
|
||||
private fun supportsOpeningCalendarContactsLocally(): Boolean = storageManager
|
||||
.getCapability(accountManager.user)
|
||||
.version
|
||||
.isNewerOrEqual(NextcloudVersion.nextcloud_30)
|
||||
|
||||
@Deprecated("Deprecated in Java")
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
val item = menu.findItem(R.id.action_search)
|
||||
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) {
|
||||
(item.actionView as? SearchView?)?.run {
|
||||
// Required to align with TextView width.
|
||||
|
@ -230,6 +260,7 @@ class UnifiedSearchFragment :
|
|||
private fun setupAdapter() {
|
||||
val gridLayoutManager = GridLayoutManager(requireContext(), 1)
|
||||
adapter = UnifiedSearchListAdapter(
|
||||
supportsOpeningCalendarContactsLocally(),
|
||||
storageManager,
|
||||
this,
|
||||
this,
|
||||
|
|
|
@ -11,7 +11,6 @@ import com.owncloud.android.lib.common.SearchResultEntry
|
|||
import com.owncloud.android.ui.unifiedsearch.ProviderID
|
||||
|
||||
interface UnifiedSearchListInterface {
|
||||
|
||||
fun onSearchResultClicked(searchResultEntry: SearchResultEntry)
|
||||
fun onLoadMoreClicked(providerID: ProviderID)
|
||||
}
|
||||
|
|
|
@ -1,15 +1,8 @@
|
|||
/*
|
||||
* Nextcloud - Android Client
|
||||
*
|
||||
* SPDX-FileCopyrightText: 2020-2024 Andy Scherzinger <info@andy-scherzinger.de>
|
||||
* SPDX-FileCopyrightText: 2023 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* 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)
|
||||
* SPDX-FileCopyrightText: 2024 Alper Ozturk <alper.ozturk@nextcloud.com>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
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.GalleryFragment
|
||||
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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings
|
||||
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.
|
||||
*/
|
||||
@Suppress("TooManyFunctions")
|
||||
class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnRemoteOperationListener, Injectable {
|
||||
private var livePhotoFile: OCFile? = null
|
||||
private var viewPager: ViewPager2? = null
|
||||
private var previewImagePagerAdapter: PreviewImagePagerAdapter? = null
|
||||
private var savedPosition = 0
|
||||
private var hasSavedPosition = false
|
||||
private var requestWaitingForBinder = false
|
||||
private var downloadFinishReceiver: DownloadFinishReceiver? = null
|
||||
private var fullScreenAnchorView: View? = null
|
||||
|
||||
private var isDownloadWorkStarted = false
|
||||
private var screenState = PreviewImageActivityState.Idle
|
||||
|
||||
@Inject
|
||||
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
|
||||
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()
|
||||
}
|
||||
|
@ -181,7 +181,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
if (position == 0 && !file.isDown) {
|
||||
// this is necessary because mViewPager.setCurrentItem(0) just after setting the
|
||||
// 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) {
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -254,11 +254,15 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
|
||||
previewImagePagerAdapter?.let {
|
||||
if (it.itemCount <= 1) {
|
||||
finish()
|
||||
backToDisplayActivity()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if (user.isPresent) {
|
||||
initViewPager(user.get())
|
||||
}
|
||||
|
||||
viewPager?.setCurrentItem(nextPosition, true)
|
||||
previewImagePagerAdapter?.delete(deletePosition)
|
||||
} else if (operation is SynchronizeFileOperation) {
|
||||
|
@ -274,26 +278,50 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
|
||||
private fun observeWorkerState() {
|
||||
WorkerStateLiveData.instance().observe(this) { state: WorkerState? ->
|
||||
if (state is WorkerState.Download) {
|
||||
Log_OC.d(TAG, "Download worker started")
|
||||
isDownloadWorkStarted = true
|
||||
when (state) {
|
||||
is WorkerState.Download -> {
|
||||
Log_OC.d(TAG, "Download worker started")
|
||||
isDownloadWorkStarted = true
|
||||
|
||||
if (requestWaitingForBinder) {
|
||||
requestWaitingForBinder = false
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
"Simulating reselection of current page after connection " +
|
||||
"of download binder"
|
||||
)
|
||||
selectPage(viewPager?.currentItem)
|
||||
if (screenState == PreviewImageActivityState.WaitingForBinder) {
|
||||
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
|
||||
}
|
||||
} else {
|
||||
Log_OC.d(TAG, "Download worker stopped")
|
||||
isDownloadWorkStarted = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun selectPageOnDownload() {
|
||||
screenState = PreviewImageActivityState.Idle
|
||||
Log_OC.d(
|
||||
TAG,
|
||||
"Simulating reselection of current page after connection " +
|
||||
"of download binder"
|
||||
)
|
||||
selectPage(viewPager?.currentItem)
|
||||
}
|
||||
|
||||
private fun onImageDownloadComplete(downloadedFile: OCFile?) {
|
||||
dismissLoadingDialog()
|
||||
screenState = PreviewImageActivityState.Idle
|
||||
file = downloadedFile
|
||||
startEditImageActivity()
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
super.onResume()
|
||||
|
||||
|
@ -316,6 +344,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
}
|
||||
|
||||
private fun backToDisplayActivity() {
|
||||
sendRefreshSearchEventBroadcast()
|
||||
finish()
|
||||
}
|
||||
|
||||
|
@ -328,7 +357,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
}
|
||||
|
||||
startActivity(intent)
|
||||
finish()
|
||||
backToDisplayActivity()
|
||||
}
|
||||
|
||||
override fun showDetails(file: OCFile, activeTab: Int) {
|
||||
|
@ -356,7 +385,7 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
val currentFile = previewImagePagerAdapter?.getFileAt(position)
|
||||
|
||||
if (!isDownloadWorkStarted) {
|
||||
requestWaitingForBinder = true
|
||||
screenState = PreviewImageActivityState.WaitingForBinder
|
||||
} else {
|
||||
if (currentFile != null) {
|
||||
if (currentFile.isEncrypted && !currentFile.isDown &&
|
||||
|
@ -457,14 +486,31 @@ class PreviewImageActivity : FileActivity(), FileFragment.ContainerActivity, OnR
|
|||
|
||||
fun startImageEditor(file: OCFile) {
|
||||
if (file.isDown) {
|
||||
val editImageIntent = Intent(this, EditImageActivity::class.java)
|
||||
editImageIntent.putExtra(EditImageActivity.EXTRA_FILE, file)
|
||||
startActivity(editImageIntent)
|
||||
startEditImageActivity()
|
||||
} else {
|
||||
showLoadingDialog(getString(R.string.preview_image_downloading_image_for_edit))
|
||||
screenState = PreviewImageActivityState.Edit
|
||||
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) {
|
||||
// TODO Auto-generated method stub
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -95,6 +95,10 @@ object PermissionUtil {
|
|||
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.
|
||||
*
|
||||
|
|
19
app/src/main/res/drawable/ic_find_in_page.xml
Normal file
19
app/src/main/res/drawable/ic_find_in_page.xml
Normal 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>
|
|
@ -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_not_copied">Tiedostoa ei voitu kopioida paikalliseen tallennustilaan</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 >= 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_scan_doc_upload">Skannaa asiakirja kameralla</string>
|
||||
|
|
|
@ -677,6 +677,7 @@
|
|||
<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="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_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>
|
||||
|
|
|
@ -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_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="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_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>
|
||||
|
@ -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_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_error_unknown_format">Unable to show image</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="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="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>
|
||||
|
|
Loading…
Reference in a new issue