mirror of
https://github.com/nextcloud/talk-android
synced 2025-06-19 03:29:28 +01:00
add chunked upload for files
Signed-off-by: Marcel Hibbe <dev@mhibbe.de>
This commit is contained in:
parent
b064190d35
commit
d230d0faf2
@ -321,6 +321,7 @@ public class TakePhotoActivity extends AppCompatActivity {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
|
Log.e(TAG, "error while taking picture", e);
|
||||||
Toast.makeText(this, R.string.take_photo_error_deleting_picture, Toast.LENGTH_SHORT).show();
|
Toast.makeText(this, R.string.take_photo_error_deleting_picture, Toast.LENGTH_SHORT).show();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -65,6 +65,7 @@ import retrofit2.http.Field;
|
|||||||
import retrofit2.http.FieldMap;
|
import retrofit2.http.FieldMap;
|
||||||
import retrofit2.http.FormUrlEncoded;
|
import retrofit2.http.FormUrlEncoded;
|
||||||
import retrofit2.http.GET;
|
import retrofit2.http.GET;
|
||||||
|
import retrofit2.http.HEAD;
|
||||||
import retrofit2.http.Header;
|
import retrofit2.http.Header;
|
||||||
import retrofit2.http.Multipart;
|
import retrofit2.http.Multipart;
|
||||||
import retrofit2.http.POST;
|
import retrofit2.http.POST;
|
||||||
@ -393,7 +394,7 @@ public interface NcApi {
|
|||||||
|
|
||||||
@FormUrlEncoded
|
@FormUrlEncoded
|
||||||
@POST
|
@POST
|
||||||
Observable<Void> createRemoteShare(@Nullable @Header("Authorization") String authorization, @Url String url,
|
Observable<GenericOverall> createRemoteShare(@Nullable @Header("Authorization") String authorization, @Url String url,
|
||||||
@Field("path") String remotePath,
|
@Field("path") String remotePath,
|
||||||
@Field("shareWith") String roomToken,
|
@Field("shareWith") String roomToken,
|
||||||
@Field("shareType") String shareType,
|
@Field("shareType") String shareType,
|
||||||
@ -420,6 +421,10 @@ public interface NcApi {
|
|||||||
@Url String url,
|
@Url String url,
|
||||||
@Body RequestBody body);
|
@Body RequestBody body);
|
||||||
|
|
||||||
|
@HEAD
|
||||||
|
Observable<Response<Void>> checkIfFileExists(@Header("Authorization") String authorization,
|
||||||
|
@Url String url);
|
||||||
|
|
||||||
@GET
|
@GET
|
||||||
Call<ResponseBody> downloadFile(@Header("Authorization") String authorization,
|
Call<ResponseBody> downloadFile(@Header("Authorization") String authorization,
|
||||||
@Url String url);
|
@Url String url);
|
||||||
|
@ -162,10 +162,10 @@ import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
|||||||
import com.nextcloud.talk.utils.ContactUtils
|
import com.nextcloud.talk.utils.ContactUtils
|
||||||
import com.nextcloud.talk.utils.DateUtils
|
import com.nextcloud.talk.utils.DateUtils
|
||||||
import com.nextcloud.talk.utils.DisplayUtils
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
|
import com.nextcloud.talk.utils.FileUtils
|
||||||
import com.nextcloud.talk.utils.ImageEmojiEditText
|
import com.nextcloud.talk.utils.ImageEmojiEditText
|
||||||
import com.nextcloud.talk.utils.MagicCharPolicy
|
import com.nextcloud.talk.utils.MagicCharPolicy
|
||||||
import com.nextcloud.talk.utils.NotificationUtils
|
import com.nextcloud.talk.utils.NotificationUtils
|
||||||
import com.nextcloud.talk.utils.UriUtils
|
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
|
||||||
@ -286,6 +286,8 @@ class ChatController(args: Bundle) :
|
|||||||
|
|
||||||
var hasChatPermission: Boolean = false
|
var hasChatPermission: Boolean = false
|
||||||
|
|
||||||
|
private var videoURI: Uri? = null
|
||||||
|
|
||||||
init {
|
init {
|
||||||
Log.d(TAG, "init ChatController: " + System.identityHashCode(this).toString())
|
Log.d(TAG, "init ChatController: " + System.identityHashCode(this).toString())
|
||||||
|
|
||||||
@ -736,7 +738,7 @@ class ChatController(args: Bundle) :
|
|||||||
// Image keyboard support
|
// Image keyboard support
|
||||||
// See: https://developer.android.com/guide/topics/text/image-keyboard
|
// See: https://developer.android.com/guide/topics/text/image-keyboard
|
||||||
(binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
|
(binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
|
||||||
uploadFiles(mutableListOf(it.toString()), false)
|
uploadFile(it.toString(), false)
|
||||||
}
|
}
|
||||||
|
|
||||||
showMicrophoneButton(true)
|
showMicrophoneButton(true)
|
||||||
@ -1175,7 +1177,7 @@ class ChatController(args: Bundle) :
|
|||||||
private fun stopAndSendAudioRecording() {
|
private fun stopAndSendAudioRecording() {
|
||||||
stopAudioRecording()
|
stopAudioRecording()
|
||||||
val uri = Uri.fromFile(File(currentVoiceRecordFile))
|
val uri = Uri.fromFile(File(currentVoiceRecordFile))
|
||||||
uploadFiles(mutableListOf(uri.toString()), true)
|
uploadFile(uri.toString(), true)
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun stopAndDiscardAudioRecording() {
|
private fun stopAndDiscardAudioRecording() {
|
||||||
@ -1361,6 +1363,7 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Throws(IllegalStateException::class)
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||||
if (resultCode != RESULT_OK && (requestCode != REQUEST_CODE_MESSAGE_SEARCH)) {
|
if (resultCode != RESULT_OK && (requestCode != REQUEST_CODE_MESSAGE_SEARCH)) {
|
||||||
Log.e(TAG, "resultCode for received intent was != ok")
|
Log.e(TAG, "resultCode for received intent was != ok")
|
||||||
@ -1405,7 +1408,7 @@ class ChatController(args: Bundle) :
|
|||||||
val filenamesWithLineBreaks = StringBuilder("\n")
|
val filenamesWithLineBreaks = StringBuilder("\n")
|
||||||
|
|
||||||
for (file in filesToUpload) {
|
for (file in filesToUpload) {
|
||||||
val filename = UriUtils.getFileName(Uri.parse(file), context)
|
val filename = FileUtils.getFileName(Uri.parse(file), context)
|
||||||
filenamesWithLineBreaks.append(filename).append("\n")
|
filenamesWithLineBreaks.append(filename).append("\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1423,7 +1426,7 @@ class ChatController(args: Bundle) :
|
|||||||
.setMessage(filenamesWithLineBreaks.toString())
|
.setMessage(filenamesWithLineBreaks.toString())
|
||||||
.setPositiveButton(R.string.nc_yes) { _, _ ->
|
.setPositiveButton(R.string.nc_yes) { _, _ ->
|
||||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
|
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
|
||||||
uploadFiles(filesToUpload, false)
|
uploadFiles(filesToUpload)
|
||||||
} else {
|
} else {
|
||||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||||
}
|
}
|
||||||
@ -1468,25 +1471,31 @@ class ChatController(args: Bundle) :
|
|||||||
BuildConfig.APPLICATION_ID,
|
BuildConfig.APPLICATION_ID,
|
||||||
File(file.absolutePath)
|
File(file.absolutePath)
|
||||||
)
|
)
|
||||||
uploadFiles(mutableListOf(shareUri.toString()), false)
|
uploadFile(shareUri.toString(), false)
|
||||||
}
|
}
|
||||||
cursor?.close()
|
cursor?.close()
|
||||||
}
|
}
|
||||||
REQUEST_CODE_PICK_CAMERA -> {
|
REQUEST_CODE_PICK_CAMERA -> {
|
||||||
if (resultCode == RESULT_OK) {
|
if (resultCode == RESULT_OK) {
|
||||||
try {
|
try {
|
||||||
checkNotNull(intent)
|
|
||||||
filesToUpload.clear()
|
filesToUpload.clear()
|
||||||
run {
|
|
||||||
checkNotNull(intent.data)
|
if (intent != null && intent.data != null) {
|
||||||
intent.data.let {
|
run {
|
||||||
filesToUpload.add(intent.data.toString())
|
intent.data.let {
|
||||||
|
filesToUpload.add(intent.data.toString())
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
require(filesToUpload.isNotEmpty())
|
||||||
|
} else if (videoURI != null) {
|
||||||
|
filesToUpload.add(videoURI.toString())
|
||||||
|
videoURI = null
|
||||||
|
} else {
|
||||||
|
throw IllegalStateException("Failed to get data from intent and uri")
|
||||||
}
|
}
|
||||||
require(filesToUpload.isNotEmpty())
|
|
||||||
|
|
||||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
|
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
|
||||||
uploadFiles(filesToUpload, false)
|
uploadFiles(filesToUpload)
|
||||||
} else {
|
} else {
|
||||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||||
}
|
}
|
||||||
@ -1549,7 +1558,7 @@ class ChatController(args: Bundle) :
|
|||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
|
Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
|
||||||
if (filesToUpload.isNotEmpty()) {
|
if (filesToUpload.isNotEmpty()) {
|
||||||
uploadFiles(filesToUpload, false)
|
uploadFiles(filesToUpload)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
Toast
|
Toast
|
||||||
@ -1579,8 +1588,9 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
} else if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
} else if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
Log.d(TAG, "launch cam activity since permission for cam has been granted")
|
Toast
|
||||||
startActivityForResult(TakePhotoActivity.createIntent(context), REQUEST_CODE_PICK_CAMERA)
|
.makeText(context, context.getString(R.string.camera_permission_granted), Toast.LENGTH_LONG)
|
||||||
|
.show()
|
||||||
} else {
|
} else {
|
||||||
Toast
|
Toast
|
||||||
.makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
|
.makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
|
||||||
@ -1589,11 +1599,17 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun uploadFiles(files: MutableList<String>, isVoiceMessage: Boolean) {
|
private fun uploadFiles(files: MutableList<String>) {
|
||||||
|
for (file in files) {
|
||||||
|
uploadFile(file, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uploadFile(fileUri: String, isVoiceMessage: Boolean) {
|
||||||
var metaData = ""
|
var metaData = ""
|
||||||
|
|
||||||
if (!hasChatPermission) {
|
if (!hasChatPermission) {
|
||||||
Log.w(TAG, "uploading file(s) is forbidden because of missing attendee permissions")
|
Log.w(TAG, "uploading file is forbidden because of missing attendee permissions")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1602,28 +1618,13 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
require(files.isNotEmpty())
|
require(fileUri.isNotEmpty())
|
||||||
val data: Data = Data.Builder()
|
UploadAndShareFilesWorker.upload(
|
||||||
.putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray())
|
fileUri,
|
||||||
.putString(
|
roomToken!!,
|
||||||
UploadAndShareFilesWorker.NC_TARGETPATH,
|
currentConversation?.displayName!!,
|
||||||
CapabilitiesUtilNew.getAttachmentFolder(conversationUser!!)
|
metaData
|
||||||
)
|
)
|
||||||
.putString(UploadAndShareFilesWorker.ROOM_TOKEN, roomToken)
|
|
||||||
.putString(UploadAndShareFilesWorker.META_DATA, metaData)
|
|
||||||
.build()
|
|
||||||
val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
|
|
||||||
.setInputData(data)
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance().enqueue(uploadWorker)
|
|
||||||
|
|
||||||
if (!isVoiceMessage) {
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.getString(R.string.nc_upload_in_progess),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
}
|
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
|
Toast.makeText(context, context.resources?.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
|
||||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
||||||
@ -2277,16 +2278,18 @@ class ChatController(args: Bundle) :
|
|||||||
val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
|
val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
|
||||||
val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
|
val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
|
||||||
|
|
||||||
for (itemWrapper in adapter?.items!!) {
|
if (adapter?.items != null) {
|
||||||
if (itemWrapper.item is ChatMessage) {
|
for (itemWrapper in adapter?.items!!) {
|
||||||
val chatMessage = itemWrapper.item as ChatMessage
|
if (itemWrapper.item is ChatMessage) {
|
||||||
if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
|
val chatMessage = itemWrapper.item as ChatMessage
|
||||||
messagesToDelete.add(chatMessage)
|
if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
|
||||||
|
messagesToDelete.add(chatMessage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
adapter!!.delete(messagesToDelete)
|
||||||
|
adapter!!.notifyDataSetChanged()
|
||||||
}
|
}
|
||||||
adapter!!.delete(messagesToDelete)
|
|
||||||
adapter!!.notifyDataSetChanged()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration")) {
|
if (CapabilitiesUtilNew.hasSpreedFeatureCapability(conversationUser, "message-expiration")) {
|
||||||
@ -3255,22 +3258,30 @@ class ChatController(args: Bundle) :
|
|||||||
}
|
}
|
||||||
|
|
||||||
fun sendVideoFromCamIntent() {
|
fun sendVideoFromCamIntent() {
|
||||||
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
|
if (!permissionUtil.isCameraPermissionGranted()) {
|
||||||
takeVideoIntent.resolveActivity(activity!!.packageManager)?.also {
|
requestCameraPermissions()
|
||||||
val videoFile: File? = try {
|
} else {
|
||||||
val outputDir = context.cacheDir
|
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
|
||||||
val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
|
takeVideoIntent.resolveActivity(activity!!.packageManager)?.also {
|
||||||
val date = dateFormat.format(Date())
|
val videoFile: File? = try {
|
||||||
File("$outputDir/$VIDEO_PREFIX_PART$date$VIDEO_SUFFIX")
|
val outputDir = context.cacheDir
|
||||||
} catch (e: IOException) {
|
val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
|
||||||
Log.e(TAG, "error while creating video file", e)
|
val date = dateFormat.format(Date())
|
||||||
null
|
val videoName = String.format(
|
||||||
}
|
context.resources.getString(R.string.nc_video_filename),
|
||||||
|
date
|
||||||
|
)
|
||||||
|
File("$outputDir/$videoName$VIDEO_SUFFIX")
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "error while creating video file", e)
|
||||||
|
null
|
||||||
|
}
|
||||||
|
|
||||||
videoFile?.also {
|
videoFile?.also {
|
||||||
val videoURI: Uri = FileProvider.getUriForFile(context, context.packageName, it)
|
videoURI = FileProvider.getUriForFile(context, context.packageName, it)
|
||||||
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
|
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
|
||||||
startActivityForResult(takeVideoIntent, REQUEST_CODE_PICK_CAMERA)
|
startActivityForResult(takeVideoIntent, REQUEST_CODE_PICK_CAMERA)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -3312,7 +3323,6 @@ class ChatController(args: Bundle) :
|
|||||||
private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
|
private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
|
||||||
private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
|
private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
|
||||||
private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
|
private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
|
||||||
private const val VIDEO_PREFIX_PART = "Talk Video "
|
|
||||||
private const val VIDEO_SUFFIX = ".mp4"
|
private const val VIDEO_SUFFIX = ".mp4"
|
||||||
private const val SHORT_VIBRATE: Long = 20
|
private const val SHORT_VIBRATE: Long = 20
|
||||||
private const val FULLY_OPAQUE_INT: Int = 255
|
private const val FULLY_OPAQUE_INT: Int = 255
|
||||||
|
@ -107,8 +107,8 @@ import com.nextcloud.talk.utils.AttendeePermissionsUtil
|
|||||||
import com.nextcloud.talk.utils.ClosedInterfaceImpl
|
import com.nextcloud.talk.utils.ClosedInterfaceImpl
|
||||||
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
||||||
import com.nextcloud.talk.utils.DisplayUtils
|
import com.nextcloud.talk.utils.DisplayUtils
|
||||||
|
import com.nextcloud.talk.utils.FileUtils
|
||||||
import com.nextcloud.talk.utils.Mimetype
|
import com.nextcloud.talk.utils.Mimetype
|
||||||
import com.nextcloud.talk.utils.UriUtils.Companion.getFileName
|
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys
|
import com.nextcloud.talk.utils.bundle.BundleKeys
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ACTIVE_CONVERSATION
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FORWARD_HIDE_SOURCE_ROOM
|
||||||
@ -121,7 +121,6 @@ import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_ID
|
|||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_SHARED_TEXT
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
|
||||||
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.getAttachmentFolder
|
|
||||||
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
|
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.hasSpreedFeatureCapability
|
||||||
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isServerEOL
|
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isServerEOL
|
||||||
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUnifiedSearchAvailable
|
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUnifiedSearchAvailable
|
||||||
@ -924,7 +923,7 @@ class ConversationsListController(bundle: Bundle) :
|
|||||||
if (isStoragePermissionGranted(context)) {
|
if (isStoragePermissionGranted(context)) {
|
||||||
val fileNamesWithLineBreaks = StringBuilder("\n")
|
val fileNamesWithLineBreaks = StringBuilder("\n")
|
||||||
for (file in filesToShare!!) {
|
for (file in filesToShare!!) {
|
||||||
val filename = getFileName(Uri.parse(file), context)
|
val filename = FileUtils.getFileName(Uri.parse(file), context)
|
||||||
fileNamesWithLineBreaks.append(filename).append("\n")
|
fileNamesWithLineBreaks.append(filename).append("\n")
|
||||||
}
|
}
|
||||||
val confirmationQuestion: String = if (filesToShare!!.size == 1) {
|
val confirmationQuestion: String = if (filesToShare!!.size == 1) {
|
||||||
@ -1042,25 +1041,14 @@ class ConversationsListController(bundle: Bundle) :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
var filesToShareArray: Array<String?> = arrayOfNulls(filesToShare!!.size)
|
filesToShare?.forEach {
|
||||||
filesToShareArray = filesToShare!!.toArray(filesToShareArray)
|
UploadAndShareFilesWorker.upload(
|
||||||
val data = Data.Builder()
|
it,
|
||||||
.putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, filesToShareArray)
|
selectedConversation!!.token!!,
|
||||||
.putString(
|
selectedConversation!!.displayName!!,
|
||||||
UploadAndShareFilesWorker.NC_TARGETPATH,
|
null
|
||||||
getAttachmentFolder(currentUser!!)
|
|
||||||
)
|
)
|
||||||
.putString(UploadAndShareFilesWorker.ROOM_TOKEN, selectedConversation!!.token)
|
}
|
||||||
.build()
|
|
||||||
val uploadWorker = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
|
|
||||||
.setInputData(data)
|
|
||||||
.build()
|
|
||||||
WorkManager.getInstance().enqueue(uploadWorker)
|
|
||||||
Toast.makeText(
|
|
||||||
context,
|
|
||||||
context.resources.getString(R.string.nc_upload_in_progess),
|
|
||||||
Toast.LENGTH_LONG
|
|
||||||
).show()
|
|
||||||
} catch (e: IllegalArgumentException) {
|
} catch (e: IllegalArgumentException) {
|
||||||
Toast.makeText(context, context.resources.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
|
Toast.makeText(context, context.resources.getString(R.string.nc_upload_failed), Toast.LENGTH_LONG).show()
|
||||||
Log.e(TAG, "Something went wrong when trying to upload file", e)
|
Log.e(TAG, "Something went wrong when trying to upload file", e)
|
||||||
|
@ -572,7 +572,7 @@ class ProfileController : BaseController(R.layout.controller_profile) {
|
|||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun createTempFileForAvatar(): File? {
|
private fun createTempFileForAvatar(): File {
|
||||||
FileUtils.removeTempCacheFile(
|
FileUtils.removeTempCacheFile(
|
||||||
this.context,
|
this.context,
|
||||||
AVATAR_PATH
|
AVATAR_PATH
|
||||||
|
@ -1,115 +0,0 @@
|
|||||||
/*
|
|
||||||
* Nextcloud Talk application
|
|
||||||
*
|
|
||||||
* @author Mario Danic
|
|
||||||
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.nextcloud.talk.jobs;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import com.nextcloud.talk.api.NcApi;
|
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication;
|
|
||||||
import com.nextcloud.talk.data.user.model.User;
|
|
||||||
import com.nextcloud.talk.users.UserManager;
|
|
||||||
import com.nextcloud.talk.utils.ApiUtils;
|
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys;
|
|
||||||
|
|
||||||
import java.util.ArrayList;
|
|
||||||
import java.util.Collections;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import javax.inject.Inject;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.work.Data;
|
|
||||||
import androidx.work.Worker;
|
|
||||||
import androidx.work.WorkerParameters;
|
|
||||||
import autodagger.AutoInjector;
|
|
||||||
import io.reactivex.Observer;
|
|
||||||
import io.reactivex.disposables.Disposable;
|
|
||||||
import io.reactivex.schedulers.Schedulers;
|
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication.class)
|
|
||||||
public class ShareOperationWorker extends Worker {
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
UserManager userManager;
|
|
||||||
|
|
||||||
@Inject
|
|
||||||
NcApi ncApi;
|
|
||||||
|
|
||||||
private final String TAG = "ShareOperationWorker";
|
|
||||||
private long userId;
|
|
||||||
private String roomToken;
|
|
||||||
private List<String> filesArray = new ArrayList<>();
|
|
||||||
private String credentials;
|
|
||||||
private String baseUrl;
|
|
||||||
private String metaData;
|
|
||||||
|
|
||||||
public ShareOperationWorker(@NonNull Context context, @NonNull WorkerParameters workerParams) {
|
|
||||||
super(context, workerParams);
|
|
||||||
NextcloudTalkApplication.Companion.getSharedApplication().getComponentApplication().inject(this);
|
|
||||||
Data data = workerParams.getInputData();
|
|
||||||
userId = data.getLong(BundleKeys.INSTANCE.getKEY_INTERNAL_USER_ID(), 0);
|
|
||||||
roomToken = data.getString(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN());
|
|
||||||
metaData = data.getString(BundleKeys.INSTANCE.getKEY_META_DATA());
|
|
||||||
Collections.addAll(filesArray, data.getStringArray(BundleKeys.INSTANCE.getKEY_FILE_PATHS()));
|
|
||||||
User operationsUser = userManager.getUserWithId(userId).blockingGet();
|
|
||||||
credentials = ApiUtils.getCredentials(operationsUser.getUsername(), operationsUser.getToken());
|
|
||||||
baseUrl = operationsUser.getBaseUrl();
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Result doWork() {
|
|
||||||
|
|
||||||
for (int i = 0; i < filesArray.size(); i++) {
|
|
||||||
ncApi.createRemoteShare(credentials,
|
|
||||||
ApiUtils.getSharingUrl(baseUrl),
|
|
||||||
filesArray.get(i),
|
|
||||||
roomToken,
|
|
||||||
"10",
|
|
||||||
metaData)
|
|
||||||
.subscribeOn(Schedulers.io())
|
|
||||||
.blockingSubscribe(new Observer<Void>() {
|
|
||||||
@Override
|
|
||||||
public void onSubscribe(Disposable d) {
|
|
||||||
// unused atm
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onNext(Void aVoid) {
|
|
||||||
// unused atm
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onError(Throwable e) {
|
|
||||||
Log.w(TAG, "error while creating RemoteShare", e);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onComplete() {
|
|
||||||
// unused atm
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return Result.success();
|
|
||||||
}
|
|
||||||
}
|
|
@ -0,0 +1,116 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Marcel Hibbe
|
||||||
|
* @author Mario Danic
|
||||||
|
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* Copyright (C) 2017-2018 Mario Danic <mario@lovelyhq.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.jobs
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.util.Log
|
||||||
|
import androidx.work.Data
|
||||||
|
import androidx.work.OneTimeWorkRequest
|
||||||
|
import androidx.work.WorkManager
|
||||||
|
import androidx.work.Worker
|
||||||
|
import androidx.work.WorkerParameters
|
||||||
|
import autodagger.AutoInjector
|
||||||
|
import com.nextcloud.talk.api.NcApi
|
||||||
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
|
import com.nextcloud.talk.application.NextcloudTalkApplication.Companion.sharedApplication
|
||||||
|
import com.nextcloud.talk.data.user.model.User
|
||||||
|
import com.nextcloud.talk.users.UserManager
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_META_DATA
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
|
class ShareOperationWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var userManager: UserManager
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var ncApi: NcApi
|
||||||
|
|
||||||
|
private val userId: Long
|
||||||
|
private val roomToken: String?
|
||||||
|
private val filesArray: MutableList<String?> = ArrayList()
|
||||||
|
private val credentials: String
|
||||||
|
private val baseUrl: String?
|
||||||
|
private val metaData: String?
|
||||||
|
|
||||||
|
override fun doWork(): Result {
|
||||||
|
for (filePath in filesArray) {
|
||||||
|
ncApi.createRemoteShare(
|
||||||
|
credentials,
|
||||||
|
ApiUtils.getSharingUrl(baseUrl),
|
||||||
|
filePath,
|
||||||
|
roomToken,
|
||||||
|
"10",
|
||||||
|
metaData
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.blockingSubscribe(
|
||||||
|
{}, { e -> Log.w(TAG, "error while creating RemoteShare", e) }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
sharedApplication!!.componentApplication.inject(this)
|
||||||
|
val data = workerParams.inputData
|
||||||
|
userId = data.getLong(KEY_INTERNAL_USER_ID, 0)
|
||||||
|
roomToken = data.getString(KEY_ROOM_TOKEN)
|
||||||
|
metaData = data.getString(KEY_META_DATA)
|
||||||
|
data.getStringArray(KEY_FILE_PATHS)?.let { filesArray.addAll(it.toList()) }
|
||||||
|
|
||||||
|
val operationsUser = userManager.getUserWithId(userId).blockingGet()
|
||||||
|
baseUrl = operationsUser.baseUrl
|
||||||
|
credentials = ApiUtils.getCredentials(operationsUser.username, operationsUser.token)
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = ShareOperationWorker::class.simpleName
|
||||||
|
|
||||||
|
fun shareFile(
|
||||||
|
roomToken: String?,
|
||||||
|
currentUser: User,
|
||||||
|
remotePath: String,
|
||||||
|
metaData: String?
|
||||||
|
) {
|
||||||
|
val paths: MutableList<String> = ArrayList()
|
||||||
|
paths.add(remotePath)
|
||||||
|
|
||||||
|
val data = Data.Builder()
|
||||||
|
.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
|
||||||
|
.putString(KEY_ROOM_TOKEN, roomToken)
|
||||||
|
.putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
|
||||||
|
.putString(KEY_META_DATA, metaData)
|
||||||
|
.build()
|
||||||
|
val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
|
||||||
|
.setInputData(data)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance().enqueue(shareWorker)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -2,7 +2,7 @@
|
|||||||
* Nextcloud Talk application
|
* Nextcloud Talk application
|
||||||
*
|
*
|
||||||
* @author Marcel Hibbe
|
* @author Marcel Hibbe
|
||||||
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
|
* Copyright (C) 2021-2022 Marcel Hibbe <dev@mhibbe.de>
|
||||||
*
|
*
|
||||||
* This program is free software: you can redistribute it and/or modify
|
* This program is free software: you can redistribute it and/or modify
|
||||||
* it under the terms of the GNU General Public License as published by
|
* it under the terms of the GNU General Public License as published by
|
||||||
@ -21,46 +21,50 @@
|
|||||||
package com.nextcloud.talk.jobs
|
package com.nextcloud.talk.jobs
|
||||||
|
|
||||||
import android.Manifest
|
import android.Manifest
|
||||||
|
import android.app.Notification
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
import android.net.Uri
|
import android.net.Uri
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.os.SystemClock
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
import androidx.core.content.PermissionChecker
|
import androidx.core.content.PermissionChecker
|
||||||
import androidx.work.Data
|
import androidx.work.Data
|
||||||
|
import androidx.work.ExistingWorkPolicy
|
||||||
import androidx.work.OneTimeWorkRequest
|
import androidx.work.OneTimeWorkRequest
|
||||||
import androidx.work.WorkManager
|
import androidx.work.WorkManager
|
||||||
import androidx.work.Worker
|
import androidx.work.Worker
|
||||||
import androidx.work.WorkerParameters
|
import androidx.work.WorkerParameters
|
||||||
import autodagger.AutoInjector
|
import autodagger.AutoInjector
|
||||||
import com.bluelinelabs.conductor.Controller
|
import com.bluelinelabs.conductor.Controller
|
||||||
|
import com.nextcloud.talk.R
|
||||||
|
import com.nextcloud.talk.activities.MainActivity
|
||||||
import com.nextcloud.talk.api.NcApi
|
import com.nextcloud.talk.api.NcApi
|
||||||
import com.nextcloud.talk.application.NextcloudTalkApplication
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
import com.nextcloud.talk.data.user.model.User
|
import com.nextcloud.talk.data.user.model.User
|
||||||
import com.nextcloud.talk.models.json.generic.GenericOverall
|
import com.nextcloud.talk.upload.chunked.ChunkedFileUploader
|
||||||
|
import com.nextcloud.talk.upload.chunked.OnDataTransferProgressListener
|
||||||
|
import com.nextcloud.talk.upload.normal.FileUploader
|
||||||
import com.nextcloud.talk.users.UserManager
|
import com.nextcloud.talk.users.UserManager
|
||||||
import com.nextcloud.talk.utils.ApiUtils
|
import com.nextcloud.talk.utils.FileUtils
|
||||||
import com.nextcloud.talk.utils.UriUtils
|
import com.nextcloud.talk.utils.NotificationUtils
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FILE_PATHS
|
import com.nextcloud.talk.utils.RemoteFileUtils
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_INTERNAL_USER_ID
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_META_DATA
|
|
||||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_ROOM_TOKEN
|
||||||
|
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_USER_ENTITY
|
||||||
|
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew
|
||||||
import com.nextcloud.talk.utils.preferences.AppPreferences
|
import com.nextcloud.talk.utils.preferences.AppPreferences
|
||||||
import io.reactivex.Observer
|
|
||||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
|
||||||
import io.reactivex.disposables.Disposable
|
|
||||||
import io.reactivex.schedulers.Schedulers
|
|
||||||
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
import okhttp3.RequestBody
|
import okhttp3.OkHttpClient
|
||||||
import retrofit2.Response
|
|
||||||
import java.io.File
|
|
||||||
import java.io.FileNotFoundException
|
|
||||||
import java.io.FileOutputStream
|
|
||||||
import java.io.InputStream
|
|
||||||
import javax.inject.Inject
|
import javax.inject.Inject
|
||||||
|
|
||||||
@AutoInjector(NextcloudTalkApplication::class)
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) :
|
class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) :
|
||||||
Worker(context, workerParameters) {
|
Worker(context, workerParameters), OnDataTransferProgressListener {
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
lateinit var ncApi: NcApi
|
lateinit var ncApi: NcApi
|
||||||
@ -71,6 +75,21 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
|||||||
@Inject
|
@Inject
|
||||||
lateinit var appPreferences: AppPreferences
|
lateinit var appPreferences: AppPreferences
|
||||||
|
|
||||||
|
@Inject
|
||||||
|
lateinit var okHttpClient: OkHttpClient
|
||||||
|
|
||||||
|
lateinit var fileName: String
|
||||||
|
|
||||||
|
private var mNotifyManager: NotificationManager? = null
|
||||||
|
private var mBuilder: NotificationCompat.Builder? = null
|
||||||
|
private lateinit var notification: Notification
|
||||||
|
private var notificationId: Int = 0
|
||||||
|
|
||||||
|
lateinit var roomToken: String
|
||||||
|
lateinit var conversationName: String
|
||||||
|
lateinit var currentUser: User
|
||||||
|
|
||||||
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
override fun doWork(): Result {
|
override fun doWork(): Result {
|
||||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||||
|
|
||||||
@ -85,139 +104,173 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
|||||||
}
|
}
|
||||||
|
|
||||||
return try {
|
return try {
|
||||||
val currentUser = userManager.currentUser.blockingGet()
|
currentUser = userManager.currentUser.blockingGet()
|
||||||
val sourcefiles = inputData.getStringArray(DEVICE_SOURCEFILES)
|
val sourceFile = inputData.getString(DEVICE_SOURCE_FILE)
|
||||||
val ncTargetpath = inputData.getString(NC_TARGETPATH)
|
roomToken = inputData.getString(ROOM_TOKEN)!!
|
||||||
val roomToken = inputData.getString(ROOM_TOKEN)
|
conversationName = inputData.getString(CONVERSATION_NAME)!!
|
||||||
val metaData = inputData.getString(META_DATA)
|
val metaData = inputData.getString(META_DATA)
|
||||||
|
|
||||||
checkNotNull(currentUser)
|
checkNotNull(currentUser)
|
||||||
checkNotNull(sourcefiles)
|
checkNotNull(sourceFile)
|
||||||
require(sourcefiles.isNotEmpty())
|
require(sourceFile.isNotEmpty())
|
||||||
checkNotNull(ncTargetpath)
|
|
||||||
checkNotNull(roomToken)
|
checkNotNull(roomToken)
|
||||||
|
|
||||||
for (index in sourcefiles.indices) {
|
val sourceFileUri = Uri.parse(sourceFile)
|
||||||
val sourceFileUri = Uri.parse(sourcefiles[index])
|
fileName = FileUtils.getFileName(sourceFileUri, context)
|
||||||
uploadFile(
|
val file = FileUtils.getFileFromUri(context, sourceFileUri)
|
||||||
currentUser!!,
|
val remotePath = getRemotePath(currentUser)
|
||||||
UploadItem(
|
val uploadSuccess: Boolean
|
||||||
sourceFileUri,
|
|
||||||
UriUtils.getFileName(sourceFileUri, context),
|
if (file != null && file.length() > CHUNK_UPLOAD_THRESHOLD_SIZE) {
|
||||||
createRequestBody(sourceFileUri)
|
Log.d(TAG, "starting chunked upload because size is " + file.length())
|
||||||
),
|
|
||||||
ncTargetpath,
|
initNotification()
|
||||||
|
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
|
||||||
|
|
||||||
|
uploadSuccess = ChunkedFileUploader(
|
||||||
|
okHttpClient!!,
|
||||||
|
currentUser,
|
||||||
roomToken,
|
roomToken,
|
||||||
metaData
|
metaData,
|
||||||
|
this
|
||||||
|
).upload(
|
||||||
|
file,
|
||||||
|
mimeType,
|
||||||
|
remotePath
|
||||||
)
|
)
|
||||||
}
|
} else {
|
||||||
Result.success()
|
Log.d(TAG, "starting normal upload (not chunked)")
|
||||||
} catch (e: IllegalStateException) {
|
|
||||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
|
||||||
Result.failure()
|
|
||||||
} catch (e: IllegalArgumentException) {
|
|
||||||
Log.e(javaClass.simpleName, "Something went wrong when trying to upload file", e)
|
|
||||||
Result.failure()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Suppress("Detekt.TooGenericExceptionCaught")
|
uploadSuccess = FileUploader(
|
||||||
private fun createRequestBody(sourcefileUri: Uri): RequestBody? {
|
context,
|
||||||
var requestBody: RequestBody? = null
|
currentUser,
|
||||||
try {
|
roomToken,
|
||||||
val input: InputStream = context.contentResolver.openInputStream(sourcefileUri)!!
|
ncApi
|
||||||
val buf = ByteArray(input.available())
|
).upload(
|
||||||
while (input.read(buf) != -1)
|
sourceFileUri,
|
||||||
requestBody = RequestBody.create("application/octet-stream".toMediaTypeOrNull(), buf)
|
fileName,
|
||||||
|
remotePath,
|
||||||
|
metaData
|
||||||
|
).blockingFirst()
|
||||||
|
}
|
||||||
|
|
||||||
|
if (uploadSuccess) {
|
||||||
|
mNotifyManager?.cancel(notificationId)
|
||||||
|
return Result.success()
|
||||||
|
}
|
||||||
|
|
||||||
|
Log.e(TAG, "Something went wrong when trying to upload file")
|
||||||
|
showFailedToUploadNotification()
|
||||||
|
return Result.failure()
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Log.e(javaClass.simpleName, "failed to create RequestBody for $sourcefileUri", e)
|
Log.e(TAG, "Something went wrong when trying to upload file", e)
|
||||||
|
showFailedToUploadNotification()
|
||||||
|
return Result.failure()
|
||||||
}
|
}
|
||||||
return requestBody
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun uploadFile(
|
private fun getRemotePath(currentUser: User): String {
|
||||||
currentUser: User,
|
var remotePath = CapabilitiesUtilNew.getAttachmentFolder(currentUser)!! + "/" + fileName
|
||||||
uploadItem: UploadItem,
|
remotePath = RemoteFileUtils.getNewPathIfFileExists(
|
||||||
ncTargetPath: String?,
|
ncApi,
|
||||||
roomToken: String?,
|
currentUser,
|
||||||
metaData: String?
|
remotePath
|
||||||
) {
|
|
||||||
ncApi.uploadFile(
|
|
||||||
ApiUtils.getCredentials(currentUser.username, currentUser.token),
|
|
||||||
ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetPath, uploadItem.fileName),
|
|
||||||
uploadItem.requestBody
|
|
||||||
)
|
)
|
||||||
.subscribeOn(Schedulers.io())
|
return remotePath
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
|
||||||
.subscribe(object : Observer<Response<GenericOverall>> {
|
|
||||||
override fun onSubscribe(d: Disposable) {
|
|
||||||
// unused atm
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onNext(t: Response<GenericOverall>) {
|
|
||||||
// unused atm
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onError(e: Throwable) {
|
|
||||||
Log.e(TAG, "failed to upload file ${uploadItem.fileName}")
|
|
||||||
}
|
|
||||||
|
|
||||||
override fun onComplete() {
|
|
||||||
shareFile(roomToken, currentUser, ncTargetPath, uploadItem.fileName, metaData)
|
|
||||||
copyFileToCache(uploadItem.uri, uploadItem.fileName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun copyFileToCache(sourceFileUri: Uri, filename: String) {
|
override fun onTransferProgress(
|
||||||
val cachedFile = File(context.cacheDir, filename)
|
percentage: Int
|
||||||
|
) {
|
||||||
|
notification = mBuilder!!
|
||||||
|
.setProgress(HUNDRED_PERCENT, percentage, false)
|
||||||
|
.setContentText(getNotificationContentText(percentage))
|
||||||
|
.build()
|
||||||
|
|
||||||
if (cachedFile.exists()) {
|
mNotifyManager!!.notify(notificationId, notification)
|
||||||
Log.d(TAG, "file is already in cache")
|
}
|
||||||
|
|
||||||
|
private fun initNotification() {
|
||||||
|
mNotifyManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
mBuilder = NotificationCompat.Builder(context, NotificationUtils.NOTIFICATION_CHANNEL_UPLOADS)
|
||||||
|
|
||||||
|
notification = mBuilder!!
|
||||||
|
.setContentTitle(context.resources.getString(R.string.nc_upload_in_progess))
|
||||||
|
.setContentText(getNotificationContentText(ZERO_PERCENT))
|
||||||
|
.setSmallIcon(R.drawable.upload_white)
|
||||||
|
.setOngoing(true)
|
||||||
|
.setProgress(HUNDRED_PERCENT, ZERO_PERCENT, false)
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_LOW)
|
||||||
|
.setContentIntent(getIntentToOpenConversation())
|
||||||
|
.build()
|
||||||
|
|
||||||
|
notificationId = SystemClock.uptimeMillis().toInt()
|
||||||
|
mNotifyManager!!.notify(notificationId, notification)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getNotificationContentText(percentage: Int): String {
|
||||||
|
return String.format(
|
||||||
|
context.resources.getString(R.string.nc_upload_notification_text),
|
||||||
|
getShortenedFileName(),
|
||||||
|
conversationName,
|
||||||
|
percentage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getShortenedFileName(): String {
|
||||||
|
return if (fileName.length > NOTIFICATION_FILE_NAME_MAX_LENGTH) {
|
||||||
|
THREE_DOTS + fileName.takeLast(NOTIFICATION_FILE_NAME_MAX_LENGTH)
|
||||||
} else {
|
} else {
|
||||||
val outputStream = FileOutputStream(cachedFile)
|
fileName
|
||||||
try {
|
|
||||||
val inputStream: InputStream? = context.contentResolver.openInputStream(sourceFileUri)
|
|
||||||
inputStream?.use { input ->
|
|
||||||
outputStream.use { output ->
|
|
||||||
input.copyTo(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e: FileNotFoundException) {
|
|
||||||
Log.w(TAG, "failed to copy file to cache", e)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun shareFile(
|
private fun getIntentToOpenConversation(): PendingIntent? {
|
||||||
roomToken: String?,
|
val bundle = Bundle()
|
||||||
currentUser: User,
|
val intent = Intent(context, MainActivity::class.java)
|
||||||
ncTargetpath: String?,
|
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||||
filename: String?,
|
|
||||||
metaData: String?
|
|
||||||
) {
|
|
||||||
val paths: MutableList<String> = ArrayList()
|
|
||||||
paths.add("$ncTargetpath/$filename")
|
|
||||||
|
|
||||||
val data = Data.Builder()
|
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||||
.putLong(KEY_INTERNAL_USER_ID, currentUser.id!!)
|
bundle.putParcelable(KEY_USER_ENTITY, currentUser)
|
||||||
.putString(KEY_ROOM_TOKEN, roomToken)
|
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
|
||||||
.putStringArray(KEY_FILE_PATHS, paths.toTypedArray())
|
|
||||||
.putString(KEY_META_DATA, metaData)
|
intent.putExtras(bundle)
|
||||||
|
|
||||||
|
val requestCode = System.currentTimeMillis().toInt()
|
||||||
|
val intentFlag: Int = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||||
|
PendingIntent.FLAG_MUTABLE
|
||||||
|
} else {
|
||||||
|
0
|
||||||
|
}
|
||||||
|
return PendingIntent.getActivity(context, requestCode, intent, intentFlag)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showFailedToUploadNotification() {
|
||||||
|
val failureTitle = context.resources.getString(R.string.nc_upload_failed_notification_title)
|
||||||
|
val failureText = String.format(
|
||||||
|
context.resources.getString(R.string.nc_upload_failed_notification_text),
|
||||||
|
fileName
|
||||||
|
)
|
||||||
|
notification = mBuilder!!
|
||||||
|
.setContentTitle(failureTitle)
|
||||||
|
.setContentText(failureText)
|
||||||
.build()
|
.build()
|
||||||
val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
|
|
||||||
.setInputData(data)
|
mNotifyManager!!.notify(notificationId, notification)
|
||||||
.build()
|
|
||||||
WorkManager.getInstance().enqueue(shareWorker)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
const val TAG = "UploadFileWorker"
|
private val TAG = UploadAndShareFilesWorker::class.simpleName
|
||||||
|
private const val DEVICE_SOURCE_FILE = "DEVICE_SOURCE_FILE"
|
||||||
|
private const val ROOM_TOKEN = "ROOM_TOKEN"
|
||||||
|
private const val CONVERSATION_NAME = "CONVERSATION_NAME"
|
||||||
|
private const val META_DATA = "META_DATA"
|
||||||
|
private const val CHUNK_UPLOAD_THRESHOLD_SIZE: Long = 1024000
|
||||||
|
private const val NOTIFICATION_FILE_NAME_MAX_LENGTH = 20
|
||||||
|
private const val THREE_DOTS = "…"
|
||||||
|
private const val HUNDRED_PERCENT = 100
|
||||||
|
private const val ZERO_PERCENT = 0
|
||||||
const val REQUEST_PERMISSION = 3123
|
const val REQUEST_PERMISSION = 3123
|
||||||
const val DEVICE_SOURCEFILES = "DEVICE_SOURCEFILES"
|
|
||||||
const val NC_TARGETPATH = "NC_TARGETPATH"
|
|
||||||
const val ROOM_TOKEN = "ROOM_TOKEN"
|
|
||||||
const val META_DATA = "META_DATA"
|
|
||||||
|
|
||||||
fun isStoragePermissionGranted(context: Context): Boolean {
|
fun isStoragePermissionGranted(context: Context): Boolean {
|
||||||
return when {
|
return when {
|
||||||
@ -276,11 +329,23 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private data class UploadItem(
|
fun upload(
|
||||||
val uri: Uri,
|
fileUri: String,
|
||||||
val fileName: String,
|
roomToken: String,
|
||||||
val requestBody: RequestBody?
|
conversationName: String,
|
||||||
)
|
metaData: String?
|
||||||
|
) {
|
||||||
|
val data: Data = Data.Builder()
|
||||||
|
.putString(DEVICE_SOURCE_FILE, fileUri)
|
||||||
|
.putString(ROOM_TOKEN, roomToken)
|
||||||
|
.putString(CONVERSATION_NAME, conversationName)
|
||||||
|
.putString(META_DATA, metaData)
|
||||||
|
.build()
|
||||||
|
val uploadWorker: OneTimeWorkRequest = OneTimeWorkRequest.Builder(UploadAndShareFilesWorker::class.java)
|
||||||
|
.setInputData(data)
|
||||||
|
.build()
|
||||||
|
WorkManager.getInstance().enqueueUniqueWork(fileUri, ExistingWorkPolicy.KEEP, uploadWorker)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
28
app/src/main/java/com/nextcloud/talk/upload/chunked/Chunk.kt
Normal file
28
app/src/main/java/com/nextcloud/talk/upload/chunked/Chunk.kt
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* @author Tobias Kaminsky
|
||||||
|
* @author Marcel Hibbe
|
||||||
|
* Copyright (C) 2022 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* Copyright (C) 2019 Tobias Kaminsky
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.upload.chunked
|
||||||
|
|
||||||
|
data class Chunk(var start: Long, var end: Long) {
|
||||||
|
fun length(): Long {
|
||||||
|
return end - start + 1
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,124 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* Copyright (C) 2020 ownCloud GmbH.
|
||||||
|
* Copyright (C) 2022 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.upload.chunked
|
||||||
|
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import okio.BufferedSink
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.nio.ByteBuffer
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Request body that represents a file chunk and include information about the progress when uploading it
|
||||||
|
*
|
||||||
|
* @author David González Verdugo
|
||||||
|
*/
|
||||||
|
class ChunkFromFileRequestBody(
|
||||||
|
file: File,
|
||||||
|
contentType: MediaType?,
|
||||||
|
channel: FileChannel?,
|
||||||
|
chunkSize: Long,
|
||||||
|
offset: Long,
|
||||||
|
listener: OnDataTransferProgressListener
|
||||||
|
) : RequestBody() {
|
||||||
|
private val mFile: File
|
||||||
|
private val mContentType: MediaType?
|
||||||
|
private val mChannel: FileChannel
|
||||||
|
private val mChunkSize: Long
|
||||||
|
private val mOffset: Long
|
||||||
|
private var mTransferred: Long
|
||||||
|
private var mDataTransferListener: OnDataTransferProgressListener
|
||||||
|
private val mBuffer = ByteBuffer.allocate(BUFFER_CAPACITY)
|
||||||
|
override fun contentLength(): Long {
|
||||||
|
return try {
|
||||||
|
mChunkSize.coerceAtMost(mChannel.size() - mOffset)
|
||||||
|
} catch (e: IOException) {
|
||||||
|
mChunkSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Throws(IOException::class)
|
||||||
|
override fun writeTo(sink: BufferedSink) {
|
||||||
|
var readCount: Int
|
||||||
|
try {
|
||||||
|
mChannel.position(mOffset)
|
||||||
|
var size = mFile.length()
|
||||||
|
if (size == 0L) {
|
||||||
|
size = -1
|
||||||
|
}
|
||||||
|
val maxCount = (mOffset + mChunkSize - 1).coerceAtMost(mChannel.size())
|
||||||
|
var percentageOld = 0
|
||||||
|
while (mChannel.position() < maxCount) {
|
||||||
|
readCount = mChannel.read(mBuffer)
|
||||||
|
sink.buffer.write(mBuffer.array(), 0, readCount)
|
||||||
|
mBuffer.clear()
|
||||||
|
if (mTransferred < maxCount) { // condition to avoid accumulate progress for repeated chunks
|
||||||
|
mTransferred += readCount.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
val percentage =
|
||||||
|
if (size > ZERO_PERCENT) (mTransferred * HUNDRED_PERCENT / size).toInt() else ZERO_PERCENT
|
||||||
|
if (percentage > percentageOld) {
|
||||||
|
percentageOld = percentage
|
||||||
|
mDataTransferListener.onTransferProgress(
|
||||||
|
percentage
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (io: IOException) {
|
||||||
|
// any read problem will be handled as if the file is not there
|
||||||
|
val fnf = java.io.FileNotFoundException("Exception reading source file")
|
||||||
|
fnf.initCause(io)
|
||||||
|
throw fnf
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun contentType(): MediaType? {
|
||||||
|
return mContentType
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = ChunkFromFileRequestBody::class.java.simpleName
|
||||||
|
private const val BUFFER_CAPACITY = 4096
|
||||||
|
private const val HUNDRED_PERCENT = 100
|
||||||
|
private const val ZERO_PERCENT = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
requireNotNull(channel) { "File may not be null" }
|
||||||
|
require(chunkSize > 0) { "Chunk size must be greater than zero" }
|
||||||
|
mFile = file
|
||||||
|
mChannel = channel
|
||||||
|
mChunkSize = chunkSize
|
||||||
|
mOffset = offset
|
||||||
|
mTransferred = offset
|
||||||
|
mDataTransferListener = listener
|
||||||
|
mContentType = contentType
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,396 @@
|
|||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
*
|
||||||
|
* Copyright (C) 2015 ownCloud Inc.
|
||||||
|
* Copyright (C) 2022 Nextcloud GmbH
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
|
||||||
|
package com.nextcloud.talk.upload.chunked
|
||||||
|
|
||||||
|
import android.net.Uri
|
||||||
|
import android.text.TextUtils
|
||||||
|
import android.util.Log
|
||||||
|
import at.bitfire.dav4jvm.DavResource
|
||||||
|
import at.bitfire.dav4jvm.Property
|
||||||
|
import at.bitfire.dav4jvm.exception.DavException
|
||||||
|
import at.bitfire.dav4jvm.exception.HttpException
|
||||||
|
import at.bitfire.dav4jvm.property.DisplayName
|
||||||
|
import at.bitfire.dav4jvm.property.GetContentType
|
||||||
|
import at.bitfire.dav4jvm.property.GetLastModified
|
||||||
|
import at.bitfire.dav4jvm.property.ResourceType
|
||||||
|
import autodagger.AutoInjector
|
||||||
|
import com.nextcloud.talk.application.NextcloudTalkApplication
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.DavResponse
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.properties.NCEncrypted
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.properties.NCPermission
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.properties.NCPreview
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.properties.OCFavorite
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.properties.OCId
|
||||||
|
import com.nextcloud.talk.components.filebrowser.models.properties.OCSize
|
||||||
|
import com.nextcloud.talk.dagger.modules.RestModule
|
||||||
|
import com.nextcloud.talk.data.user.model.User
|
||||||
|
import com.nextcloud.talk.jobs.ShareOperationWorker
|
||||||
|
import com.nextcloud.talk.remotefilebrowser.model.RemoteFileBrowserItem
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
|
import com.nextcloud.talk.utils.FileUtils
|
||||||
|
import com.nextcloud.talk.utils.Mimetype
|
||||||
|
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
|
||||||
|
import okhttp3.MediaType
|
||||||
|
import okhttp3.OkHttpClient
|
||||||
|
import okhttp3.Response
|
||||||
|
import java.io.File
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.RandomAccessFile
|
||||||
|
import java.nio.channels.FileChannel
|
||||||
|
import java.util.Locale
|
||||||
|
|
||||||
|
@AutoInjector(NextcloudTalkApplication::class)
|
||||||
|
class ChunkedFileUploader(
|
||||||
|
okHttpClient: OkHttpClient,
|
||||||
|
val currentUser: User,
|
||||||
|
val roomToken: String,
|
||||||
|
val metaData: String?,
|
||||||
|
val listener: OnDataTransferProgressListener
|
||||||
|
) {
|
||||||
|
|
||||||
|
private var okHttpClientNoRedirects: OkHttpClient? = null
|
||||||
|
private var remoteChunkUrl: String
|
||||||
|
|
||||||
|
init {
|
||||||
|
initHttpClient(okHttpClient, currentUser)
|
||||||
|
remoteChunkUrl = ApiUtils.getUrlForChunkedUpload(currentUser.baseUrl, currentUser.userId)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
|
fun upload(
|
||||||
|
localFile: File,
|
||||||
|
mimeType: MediaType?,
|
||||||
|
targetPath: String
|
||||||
|
): Boolean {
|
||||||
|
try {
|
||||||
|
val uploadFolderUri: String = remoteChunkUrl + "/" + FileUtils.md5Sum(localFile)
|
||||||
|
val davResource = DavResource(
|
||||||
|
okHttpClientNoRedirects!!,
|
||||||
|
uploadFolderUri.toHttpUrlOrNull()!!
|
||||||
|
)
|
||||||
|
|
||||||
|
createFolder(davResource)
|
||||||
|
|
||||||
|
val chunksOnServer: MutableList<Chunk> = getUploadedChunks(davResource, uploadFolderUri)
|
||||||
|
Log.d(TAG, "chunksOnServer: " + chunksOnServer.size)
|
||||||
|
|
||||||
|
val missingChunks: List<Chunk> = checkMissingChunks(chunksOnServer, localFile.length())
|
||||||
|
Log.d(TAG, "missingChunks: " + missingChunks.size)
|
||||||
|
|
||||||
|
for (missingChunk in missingChunks) {
|
||||||
|
uploadChunk(localFile, uploadFolderUri, mimeType, missingChunk, missingChunk.length())
|
||||||
|
}
|
||||||
|
|
||||||
|
assembleChunks(uploadFolderUri, targetPath)
|
||||||
|
return true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Something went wrong in ChunkedFileUploader", e)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.ThrowsCount")
|
||||||
|
private fun createFolder(davResource: DavResource) {
|
||||||
|
try {
|
||||||
|
davResource.mkCol(
|
||||||
|
xmlBody = null,
|
||||||
|
) { response: Response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IOException("failed to create folder. response code: " + response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw IOException("failed to create folder", e)
|
||||||
|
} catch (e: HttpException) {
|
||||||
|
if (e.code == METHOD_NOT_ALLOWED_CODE) {
|
||||||
|
Log.d(TAG, "Folder most probably already exists, that's okay, just continue..")
|
||||||
|
} else {
|
||||||
|
throw IOException("failed to create folder", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.ComplexMethod")
|
||||||
|
private fun getUploadedChunks(
|
||||||
|
davResource: DavResource,
|
||||||
|
uploadFolderUri: String
|
||||||
|
): MutableList<Chunk> {
|
||||||
|
val davResponse = DavResponse()
|
||||||
|
val memberElements: MutableList<at.bitfire.dav4jvm.Response> = ArrayList()
|
||||||
|
val rootElement = arrayOfNulls<at.bitfire.dav4jvm.Response>(1)
|
||||||
|
val remoteFiles: MutableList<RemoteFileBrowserItem> = ArrayList()
|
||||||
|
try {
|
||||||
|
davResource.propfind(
|
||||||
|
1
|
||||||
|
) { response: at.bitfire.dav4jvm.Response, hrefRelation: at.bitfire.dav4jvm.Response.HrefRelation? ->
|
||||||
|
davResponse.setResponse(response)
|
||||||
|
when (hrefRelation) {
|
||||||
|
at.bitfire.dav4jvm.Response.HrefRelation.MEMBER -> memberElements.add(response)
|
||||||
|
at.bitfire.dav4jvm.Response.HrefRelation.SELF -> rootElement[0] = response
|
||||||
|
at.bitfire.dav4jvm.Response.HrefRelation.OTHER -> {}
|
||||||
|
else -> {}
|
||||||
|
}
|
||||||
|
Unit
|
||||||
|
}
|
||||||
|
} catch (e: IOException) {
|
||||||
|
throw IOException("Error reading remote path", e)
|
||||||
|
} catch (e: DavException) {
|
||||||
|
throw IOException("Error reading remote path", e)
|
||||||
|
}
|
||||||
|
for (memberElement in memberElements) {
|
||||||
|
remoteFiles.add(
|
||||||
|
getModelFromResponse(
|
||||||
|
memberElement,
|
||||||
|
memberElement
|
||||||
|
.href
|
||||||
|
.toString()
|
||||||
|
.substring(uploadFolderUri.length)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val chunksOnServer: MutableList<Chunk> = ArrayList()
|
||||||
|
|
||||||
|
for (remoteFile in remoteFiles) {
|
||||||
|
if (!".file".equals(remoteFile.displayName, ignoreCase = true) && remoteFile.isFile) {
|
||||||
|
val part: List<String> = remoteFile.displayName!!.split("-")
|
||||||
|
chunksOnServer.add(
|
||||||
|
Chunk(
|
||||||
|
part[0].toLong(),
|
||||||
|
part[1].toLong()
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return chunksOnServer
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun checkMissingChunks(chunks: List<Chunk>, length: Long): List<Chunk> {
|
||||||
|
val missingChunks: MutableList<Chunk> = java.util.ArrayList()
|
||||||
|
var start: Long = 0
|
||||||
|
while (start <= length) {
|
||||||
|
val nextChunk: Chunk? = findNextFittingChunk(chunks, start)
|
||||||
|
if (nextChunk == null) {
|
||||||
|
// create new chunk
|
||||||
|
val end: Long = if (start + CHUNK_SIZE <= length) {
|
||||||
|
start + CHUNK_SIZE - 1
|
||||||
|
} else {
|
||||||
|
length
|
||||||
|
}
|
||||||
|
missingChunks.add(Chunk(start, end))
|
||||||
|
start = end + 1
|
||||||
|
} else if (nextChunk.start == start) {
|
||||||
|
// go to next
|
||||||
|
start += nextChunk.length()
|
||||||
|
} else {
|
||||||
|
// fill the gap
|
||||||
|
missingChunks.add(Chunk(start, nextChunk.start - 1))
|
||||||
|
start = nextChunk.start
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return missingChunks
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun findNextFittingChunk(chunks: List<Chunk>, start: Long): Chunk? {
|
||||||
|
for (chunk in chunks) {
|
||||||
|
if (chunk.start >= start && chunk.start - start <= CHUNK_SIZE) {
|
||||||
|
return chunk
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun uploadChunk(
|
||||||
|
localFile: File,
|
||||||
|
uploadFolderUri: String,
|
||||||
|
mimeType: MediaType?,
|
||||||
|
chunk: Chunk,
|
||||||
|
chunkSize: Long
|
||||||
|
) {
|
||||||
|
val startString = java.lang.String.format(Locale.ROOT, "%016d", chunk.start)
|
||||||
|
val endString = java.lang.String.format(Locale.ROOT, "%016d", chunk.end)
|
||||||
|
|
||||||
|
var raf: RandomAccessFile? = null
|
||||||
|
var channel: FileChannel? = null
|
||||||
|
try {
|
||||||
|
raf = RandomAccessFile(localFile, "r")
|
||||||
|
channel = raf.channel
|
||||||
|
|
||||||
|
// Log.d(TAG, "chunkSize:$chunkSize")
|
||||||
|
// Log.d(TAG, "chunk.length():${chunk.length()}")
|
||||||
|
// Log.d(TAG, "chunk.start:${chunk.start}")
|
||||||
|
// Log.d(TAG, "chunk.end:${chunk.end}")
|
||||||
|
|
||||||
|
val chunkFromFileRequestBody = ChunkFromFileRequestBody(
|
||||||
|
localFile,
|
||||||
|
mimeType,
|
||||||
|
channel,
|
||||||
|
chunkSize,
|
||||||
|
chunk.start,
|
||||||
|
listener
|
||||||
|
)
|
||||||
|
|
||||||
|
val chunkUri = "$uploadFolderUri/$startString-$endString"
|
||||||
|
|
||||||
|
val davResource = DavResource(
|
||||||
|
okHttpClientNoRedirects!!,
|
||||||
|
chunkUri.toHttpUrlOrNull()!!
|
||||||
|
)
|
||||||
|
davResource.put(
|
||||||
|
chunkFromFileRequestBody
|
||||||
|
) { response: Response ->
|
||||||
|
if (!response.isSuccessful) {
|
||||||
|
throw IOException("Failed to upload chunk. response code: " + response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
if (channel != null) try {
|
||||||
|
channel.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error closing file channel!", e)
|
||||||
|
}
|
||||||
|
if (raf != null) {
|
||||||
|
try {
|
||||||
|
raf.close()
|
||||||
|
} catch (e: IOException) {
|
||||||
|
Log.e(TAG, "Error closing file access!", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// @RequiresApi(Build.VERSION_CODES.O)
|
||||||
|
private fun initHttpClient(okHttpClient: OkHttpClient, currentUser: User) {
|
||||||
|
val okHttpClientBuilder: OkHttpClient.Builder = okHttpClient.newBuilder()
|
||||||
|
okHttpClientBuilder.followRedirects(false)
|
||||||
|
okHttpClientBuilder.followSslRedirects(false)
|
||||||
|
// okHttpClientBuilder.readTimeout(Duration.ofMinutes(30)) // TODO set timeout
|
||||||
|
okHttpClientBuilder.authenticator(
|
||||||
|
RestModule.MagicAuthenticator(
|
||||||
|
ApiUtils.getCredentials(
|
||||||
|
currentUser.username,
|
||||||
|
currentUser.token
|
||||||
|
),
|
||||||
|
"Authorization"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
this.okHttpClientNoRedirects = okHttpClientBuilder.build()
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun assembleChunks(uploadFolderUri: String, targetPath: String) {
|
||||||
|
|
||||||
|
val destinationUri: String = ApiUtils.getUrlForFileUpload(
|
||||||
|
currentUser.baseUrl,
|
||||||
|
currentUser.userId,
|
||||||
|
targetPath
|
||||||
|
)
|
||||||
|
val originUri = "$uploadFolderUri/.file"
|
||||||
|
|
||||||
|
DavResource(
|
||||||
|
okHttpClientNoRedirects!!,
|
||||||
|
originUri.toHttpUrlOrNull()!!
|
||||||
|
).move(
|
||||||
|
destinationUri.toHttpUrlOrNull()!!,
|
||||||
|
true
|
||||||
|
) { response: Response ->
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
ShareOperationWorker.shareFile(
|
||||||
|
roomToken,
|
||||||
|
currentUser,
|
||||||
|
targetPath,
|
||||||
|
metaData
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
throw IOException("Failed to assemble chunks. response code: " + response.code)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getModelFromResponse(response: at.bitfire.dav4jvm.Response, remotePath: String): RemoteFileBrowserItem {
|
||||||
|
val remoteFileBrowserItem = RemoteFileBrowserItem()
|
||||||
|
remoteFileBrowserItem.path = Uri.decode(remotePath)
|
||||||
|
remoteFileBrowserItem.displayName = Uri.decode(File(remotePath).name)
|
||||||
|
val properties = response.properties
|
||||||
|
for (property in properties) {
|
||||||
|
mapPropertyToBrowserFile(property, remoteFileBrowserItem)
|
||||||
|
}
|
||||||
|
if (remoteFileBrowserItem.permissions != null &&
|
||||||
|
remoteFileBrowserItem.permissions!!.contains(READ_PERMISSION)
|
||||||
|
) {
|
||||||
|
remoteFileBrowserItem.isAllowedToReShare = true
|
||||||
|
}
|
||||||
|
if (TextUtils.isEmpty(remoteFileBrowserItem.mimeType) && !remoteFileBrowserItem.isFile) {
|
||||||
|
remoteFileBrowserItem.mimeType = Mimetype.FOLDER
|
||||||
|
}
|
||||||
|
|
||||||
|
return remoteFileBrowserItem
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.ComplexMethod")
|
||||||
|
private fun mapPropertyToBrowserFile(property: Property, remoteFileBrowserItem: RemoteFileBrowserItem) {
|
||||||
|
when (property) {
|
||||||
|
is OCId -> {
|
||||||
|
remoteFileBrowserItem.remoteId = property.ocId
|
||||||
|
}
|
||||||
|
is ResourceType -> {
|
||||||
|
remoteFileBrowserItem.isFile = !property.types.contains(ResourceType.COLLECTION)
|
||||||
|
}
|
||||||
|
is GetLastModified -> {
|
||||||
|
remoteFileBrowserItem.modifiedTimestamp = property.lastModified
|
||||||
|
}
|
||||||
|
is GetContentType -> {
|
||||||
|
remoteFileBrowserItem.mimeType = property.type
|
||||||
|
}
|
||||||
|
is OCSize -> {
|
||||||
|
remoteFileBrowserItem.size = property.ocSize
|
||||||
|
}
|
||||||
|
is NCPreview -> {
|
||||||
|
remoteFileBrowserItem.hasPreview = property.isNcPreview
|
||||||
|
}
|
||||||
|
is OCFavorite -> {
|
||||||
|
remoteFileBrowserItem.isFavorite = property.isOcFavorite
|
||||||
|
}
|
||||||
|
is DisplayName -> {
|
||||||
|
remoteFileBrowserItem.displayName = property.displayName
|
||||||
|
}
|
||||||
|
is NCEncrypted -> {
|
||||||
|
remoteFileBrowserItem.isEncrypted = property.isNcEncrypted
|
||||||
|
}
|
||||||
|
is NCPermission -> {
|
||||||
|
remoteFileBrowserItem.permissions = property.ncPermission
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = ChunkedFileUploader::class.simpleName
|
||||||
|
private const val READ_PERMISSION = "R"
|
||||||
|
private const val CHUNK_SIZE: Long = 1024000
|
||||||
|
private const val METHOD_NOT_ALLOWED_CODE: Int = 405
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,31 @@
|
|||||||
|
/* ownCloud Android Library is available under MIT license
|
||||||
|
* Copyright (C) 2015 ownCloud Inc.
|
||||||
|
* Copyright (C) 2012 Bartek Przybylski
|
||||||
|
*
|
||||||
|
* Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
* of this software and associated documentation files (the "Software"), to deal
|
||||||
|
* in the Software without restriction, including without limitation the rights
|
||||||
|
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
* copies of the Software, and to permit persons to whom the Software is
|
||||||
|
* furnished to do so, subject to the following conditions:
|
||||||
|
*
|
||||||
|
* The above copyright notice and this permission notice shall be included in
|
||||||
|
* all copies or substantial portions of the Software.
|
||||||
|
*
|
||||||
|
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
* NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
|
||||||
|
* BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
|
||||||
|
* ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||||
|
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||||
|
* THE SOFTWARE.
|
||||||
|
*
|
||||||
|
*/
|
||||||
|
package com.nextcloud.talk.upload.chunked
|
||||||
|
|
||||||
|
interface OnDataTransferProgressListener {
|
||||||
|
fun onTransferProgress(
|
||||||
|
percentage: Int
|
||||||
|
)
|
||||||
|
}
|
@ -0,0 +1,69 @@
|
|||||||
|
package com.nextcloud.talk.upload.normal
|
||||||
|
|
||||||
|
import android.content.Context
|
||||||
|
import android.net.Uri
|
||||||
|
import android.util.Log
|
||||||
|
import com.nextcloud.talk.api.NcApi
|
||||||
|
import com.nextcloud.talk.data.user.model.User
|
||||||
|
import com.nextcloud.talk.jobs.ShareOperationWorker
|
||||||
|
import com.nextcloud.talk.utils.ApiUtils
|
||||||
|
import com.nextcloud.talk.utils.FileUtils
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
import okhttp3.MediaType.Companion.toMediaTypeOrNull
|
||||||
|
import okhttp3.RequestBody
|
||||||
|
import java.io.InputStream
|
||||||
|
|
||||||
|
class FileUploader(
|
||||||
|
val context: Context,
|
||||||
|
val currentUser: User,
|
||||||
|
val roomToken: String,
|
||||||
|
val ncApi: NcApi
|
||||||
|
) {
|
||||||
|
fun upload(
|
||||||
|
sourceFileUri: Uri,
|
||||||
|
fileName: String,
|
||||||
|
remotePath: String,
|
||||||
|
metaData: String?
|
||||||
|
): Observable<Boolean> {
|
||||||
|
return ncApi.uploadFile(
|
||||||
|
ApiUtils.getCredentials(currentUser.username, currentUser.token),
|
||||||
|
ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, remotePath),
|
||||||
|
createRequestBody(sourceFileUri)
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread()).map { response ->
|
||||||
|
if (response.isSuccessful) {
|
||||||
|
ShareOperationWorker.shareFile(
|
||||||
|
roomToken,
|
||||||
|
currentUser,
|
||||||
|
remotePath,
|
||||||
|
metaData
|
||||||
|
)
|
||||||
|
FileUtils.copyFileToCache(context, sourceFileUri, fileName)
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("Detekt.TooGenericExceptionCaught")
|
||||||
|
private fun createRequestBody(sourceFileUri: Uri): RequestBody? {
|
||||||
|
var requestBody: RequestBody? = null
|
||||||
|
try {
|
||||||
|
val input: InputStream = context.contentResolver.openInputStream(sourceFileUri)!!
|
||||||
|
val buf = ByteArray(input.available())
|
||||||
|
while (input.read(buf) != -1)
|
||||||
|
requestBody = RequestBody.create("application/octet-stream".toMediaTypeOrNull(), buf)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "failed to create RequestBody for $sourceFileUri", e)
|
||||||
|
}
|
||||||
|
return requestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = FileUploader::class.simpleName
|
||||||
|
}
|
||||||
|
}
|
@ -400,8 +400,12 @@ public class ApiUtils {
|
|||||||
return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone";
|
return baseUrl + ocsApiVersion + "/cloud/users/search/by-phone";
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUrlForFileUpload(String baseUrl, String user, String attachmentFolder, String filename) {
|
public static String getUrlForFileUpload(String baseUrl, String user, String remotePath) {
|
||||||
return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename;
|
return baseUrl + "/remote.php/dav/files/" + user + remotePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static String getUrlForChunkedUpload(String baseUrl, String user) {
|
||||||
|
return baseUrl + "/remote.php/dav/uploads/" + user;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static String getUrlForFileDownload(String baseUrl, String user, String remotePath) {
|
public static String getUrlForFileDownload(String baseUrl, String user, String remotePath) {
|
||||||
|
@ -1,85 +0,0 @@
|
|||||||
/*
|
|
||||||
* Nextcloud Talk application
|
|
||||||
*
|
|
||||||
* @author Andy Scherzinger
|
|
||||||
* @author Stefan Niedermann
|
|
||||||
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
|
||||||
* Copyright (C) 2021 Stefan Niedermann <info@niedermann.it>
|
|
||||||
*
|
|
||||||
* This program is free software: you can redistribute it and/or modify
|
|
||||||
* it under the terms of the GNU General Public License as published by
|
|
||||||
* the Free Software Foundation, either version 3 of the License, or
|
|
||||||
* at your option) any later version.
|
|
||||||
*
|
|
||||||
* This program is distributed in the hope that it will be useful,
|
|
||||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
* GNU General Public License for more details.
|
|
||||||
*
|
|
||||||
* You should have received a copy of the GNU General Public License
|
|
||||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package com.nextcloud.talk.utils;
|
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.util.Log;
|
|
||||||
|
|
||||||
import java.io.File;
|
|
||||||
import java.io.FileNotFoundException;
|
|
||||||
import java.io.IOException;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
|
|
||||||
public class FileUtils {
|
|
||||||
private static final String TAG = FileUtils.class.getSimpleName();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new {@link File}
|
|
||||||
*/
|
|
||||||
public static File getTempCacheFile(@NonNull Context context, String fileName) throws IOException {
|
|
||||||
File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName);
|
|
||||||
|
|
||||||
Log.v(TAG, "Full path for new cache file:" + cacheFile.getAbsolutePath());
|
|
||||||
|
|
||||||
final File tempDir = cacheFile.getParentFile();
|
|
||||||
if (tempDir == null) {
|
|
||||||
throw new FileNotFoundException("could not cacheFile.getParentFile()");
|
|
||||||
}
|
|
||||||
if (!tempDir.exists()) {
|
|
||||||
Log.v(TAG,
|
|
||||||
"The folder in which the new file should be created does not exist yet. Trying to create it…");
|
|
||||||
if (tempDir.mkdirs()) {
|
|
||||||
Log.v(TAG, "Creation successful");
|
|
||||||
} else {
|
|
||||||
throw new IOException("Directory for temporary file does not exist and could not be created.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.v(TAG, "- Try to create actual cache file");
|
|
||||||
if (cacheFile.createNewFile()) {
|
|
||||||
Log.v(TAG, "Successfully created cache file");
|
|
||||||
} else {
|
|
||||||
throw new IOException("Failed to create cacheFile");
|
|
||||||
}
|
|
||||||
|
|
||||||
return cacheFile;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new {@link File}
|
|
||||||
*/
|
|
||||||
public static void removeTempCacheFile(@NonNull Context context, String fileName) throws IOException {
|
|
||||||
File cacheFile = new File(context.getApplicationContext().getFilesDir().getAbsolutePath() + "/" + fileName);
|
|
||||||
|
|
||||||
Log.v(TAG, "Full path for new cache file:" + cacheFile.getAbsolutePath());
|
|
||||||
|
|
||||||
if (cacheFile.exists()) {
|
|
||||||
if(cacheFile.delete()) {
|
|
||||||
Log.v(TAG, "Deletion successful");
|
|
||||||
} else {
|
|
||||||
throw new IOException("Directory for temporary file does not exist and could not be created.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
172
app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
Normal file
172
app/src/main/java/com/nextcloud/talk/utils/FileUtils.kt
Normal file
@ -0,0 +1,172 @@
|
|||||||
|
package com.nextcloud.talk.utils
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
* @author Marcel Hibbe
|
||||||
|
* @author Andy Scherzinger
|
||||||
|
* @author Stefan Niedermann
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author Chris Narkiewicz
|
||||||
|
* Copyright (C) 2021 Andy Scherzinger <info@andy-scherzinger.de>
|
||||||
|
* Copyright (C) 2021 Stefan Niedermann <info@niedermann.it>
|
||||||
|
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* Copyright (C) 2016 ownCloud GmbH.
|
||||||
|
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import android.content.ContentResolver
|
||||||
|
import android.content.Context
|
||||||
|
import android.database.Cursor
|
||||||
|
import android.net.Uri
|
||||||
|
import android.provider.OpenableColumns
|
||||||
|
import android.util.Log
|
||||||
|
import java.io.File
|
||||||
|
import java.io.FileNotFoundException
|
||||||
|
import java.io.FileOutputStream
|
||||||
|
import java.io.IOException
|
||||||
|
import java.io.InputStream
|
||||||
|
import java.math.BigInteger
|
||||||
|
import java.security.MessageDigest
|
||||||
|
|
||||||
|
object FileUtils {
|
||||||
|
private val TAG = FileUtils::class.java.simpleName
|
||||||
|
private const val RADIX: Int = 16
|
||||||
|
private const val MD5_LENGTH: Int = 32
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new [File]
|
||||||
|
*/
|
||||||
|
@Suppress("ThrowsCount")
|
||||||
|
@JvmStatic
|
||||||
|
fun getTempCacheFile(context: Context, fileName: String): File {
|
||||||
|
val cacheFile = File(context.applicationContext.filesDir.absolutePath + "/" + fileName)
|
||||||
|
Log.v(TAG, "Full path for new cache file:" + cacheFile.absolutePath)
|
||||||
|
val tempDir = cacheFile.parentFile ?: throw FileNotFoundException("could not cacheFile.getParentFile()")
|
||||||
|
if (!tempDir.exists()) {
|
||||||
|
Log.v(
|
||||||
|
TAG,
|
||||||
|
"The folder in which the new file should be created does not exist yet. Trying to create it…"
|
||||||
|
)
|
||||||
|
if (tempDir.mkdirs()) {
|
||||||
|
Log.v(TAG, "Creation successful")
|
||||||
|
} else {
|
||||||
|
throw IOException("Directory for temporary file does not exist and could not be created.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Log.v(TAG, "- Try to create actual cache file")
|
||||||
|
if (cacheFile.createNewFile()) {
|
||||||
|
Log.v(TAG, "Successfully created cache file")
|
||||||
|
} else {
|
||||||
|
throw IOException("Failed to create cacheFile")
|
||||||
|
}
|
||||||
|
return cacheFile
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new [File]
|
||||||
|
*/
|
||||||
|
fun removeTempCacheFile(context: Context, fileName: String) {
|
||||||
|
val cacheFile = File(context.applicationContext.filesDir.absolutePath + "/" + fileName)
|
||||||
|
Log.v(TAG, "Full path for new cache file:" + cacheFile.absolutePath)
|
||||||
|
if (cacheFile.exists()) {
|
||||||
|
if (cacheFile.delete()) {
|
||||||
|
Log.v(TAG, "Deletion successful")
|
||||||
|
} else {
|
||||||
|
throw IOException("Directory for temporary file does not exist and could not be created.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("ThrowsCount")
|
||||||
|
fun getFileFromUri(context: Context, sourceFileUri: Uri): File? {
|
||||||
|
val fileName = getFileName(sourceFileUri, context)
|
||||||
|
val scheme = sourceFileUri.scheme
|
||||||
|
|
||||||
|
val file = if (scheme == null) {
|
||||||
|
Log.d(TAG, "relative uri: " + sourceFileUri.path)
|
||||||
|
throw IllegalArgumentException("relative paths are not supported")
|
||||||
|
} else if (ContentResolver.SCHEME_CONTENT == scheme) {
|
||||||
|
copyFileToCache(context, sourceFileUri, fileName)
|
||||||
|
} else if (ContentResolver.SCHEME_FILE == scheme) {
|
||||||
|
if (sourceFileUri.path != null) {
|
||||||
|
sourceFileUri.path?.let { File(it) }
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("uri does not contain path")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
throw IllegalArgumentException("unsupported scheme: " + sourceFileUri.path)
|
||||||
|
}
|
||||||
|
return file
|
||||||
|
}
|
||||||
|
|
||||||
|
@Suppress("NestedBlockDepth")
|
||||||
|
fun copyFileToCache(context: Context, sourceFileUri: Uri, filename: String): File {
|
||||||
|
val cachedFile = File(context.cacheDir, filename)
|
||||||
|
|
||||||
|
if (cachedFile.exists()) {
|
||||||
|
Log.d(TAG, "file is already in cache")
|
||||||
|
} else {
|
||||||
|
val outputStream = FileOutputStream(cachedFile)
|
||||||
|
try {
|
||||||
|
val inputStream: InputStream? = context.contentResolver.openInputStream(sourceFileUri)
|
||||||
|
inputStream?.use { input ->
|
||||||
|
outputStream.use { output ->
|
||||||
|
input.copyTo(output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outputStream.flush()
|
||||||
|
} catch (e: FileNotFoundException) {
|
||||||
|
Log.w(TAG, "failed to copy file to cache", e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return cachedFile
|
||||||
|
}
|
||||||
|
|
||||||
|
fun getFileName(uri: Uri, context: Context?): String {
|
||||||
|
var filename: String? = null
|
||||||
|
if (uri.scheme == "content" && context != null) {
|
||||||
|
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
||||||
|
try {
|
||||||
|
if (cursor != null && cursor.moveToFirst()) {
|
||||||
|
filename = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
cursor?.close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// if it was no content uri, read filename from path
|
||||||
|
if (filename == null) {
|
||||||
|
filename = uri.path
|
||||||
|
val lastIndexOfSlash = filename!!.lastIndexOf('/')
|
||||||
|
if (lastIndexOfSlash != -1) {
|
||||||
|
filename = filename.substring(lastIndexOfSlash + 1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
@JvmStatic
|
||||||
|
fun md5Sum(file: File): String {
|
||||||
|
val temp = file.name + file.lastModified() + file.length()
|
||||||
|
val messageDigest = MessageDigest.getInstance("MD5")
|
||||||
|
messageDigest.update(temp.toByteArray())
|
||||||
|
val digest = messageDigest.digest()
|
||||||
|
val md5String = StringBuilder(BigInteger(1, digest).toString(RADIX))
|
||||||
|
while (md5String.length < MD5_LENGTH) {
|
||||||
|
md5String.insert(0, "0")
|
||||||
|
}
|
||||||
|
return md5String.toString()
|
||||||
|
}
|
||||||
|
}
|
@ -61,6 +61,7 @@ object NotificationUtils {
|
|||||||
|
|
||||||
const val NOTIFICATION_CHANNEL_MESSAGES_V4 = "NOTIFICATION_CHANNEL_MESSAGES_V4"
|
const val NOTIFICATION_CHANNEL_MESSAGES_V4 = "NOTIFICATION_CHANNEL_MESSAGES_V4"
|
||||||
const val NOTIFICATION_CHANNEL_CALLS_V4 = "NOTIFICATION_CHANNEL_CALLS_V4"
|
const val NOTIFICATION_CHANNEL_CALLS_V4 = "NOTIFICATION_CHANNEL_CALLS_V4"
|
||||||
|
const val NOTIFICATION_CHANNEL_UPLOADS = "NOTIFICATION_CHANNEL_UPLOADS"
|
||||||
|
|
||||||
const val DEFAULT_CALL_RINGTONE_URI =
|
const val DEFAULT_CALL_RINGTONE_URI =
|
||||||
"android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_call"
|
"android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_call"
|
||||||
@ -75,7 +76,7 @@ object NotificationUtils {
|
|||||||
context: Context,
|
context: Context,
|
||||||
notificationChannel: Channel,
|
notificationChannel: Channel,
|
||||||
sound: Uri?,
|
sound: Uri?,
|
||||||
audioAttributes: AudioAttributes
|
audioAttributes: AudioAttributes?
|
||||||
) {
|
) {
|
||||||
|
|
||||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
@ -84,9 +85,16 @@ object NotificationUtils {
|
|||||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||||
notificationManager.getNotificationChannel(notificationChannel.id) == null
|
notificationManager.getNotificationChannel(notificationChannel.id) == null
|
||||||
) {
|
) {
|
||||||
val channel = NotificationChannel(
|
val importance = if (notificationChannel.isImportant) {
|
||||||
notificationChannel.id, notificationChannel.name,
|
|
||||||
NotificationManager.IMPORTANCE_HIGH
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
} else {
|
||||||
|
NotificationManager.IMPORTANCE_LOW
|
||||||
|
}
|
||||||
|
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
notificationChannel.id,
|
||||||
|
notificationChannel.name,
|
||||||
|
importance
|
||||||
)
|
)
|
||||||
|
|
||||||
channel.description = notificationChannel.description
|
channel.description = notificationChannel.description
|
||||||
@ -115,7 +123,8 @@ object NotificationUtils {
|
|||||||
Channel(
|
Channel(
|
||||||
NOTIFICATION_CHANNEL_CALLS_V4,
|
NOTIFICATION_CHANNEL_CALLS_V4,
|
||||||
context.resources.getString(R.string.nc_notification_channel_calls),
|
context.resources.getString(R.string.nc_notification_channel_calls),
|
||||||
context.resources.getString(R.string.nc_notification_channel_calls_description)
|
context.resources.getString(R.string.nc_notification_channel_calls_description),
|
||||||
|
true
|
||||||
),
|
),
|
||||||
soundUri,
|
soundUri,
|
||||||
audioAttributes
|
audioAttributes
|
||||||
@ -138,19 +147,37 @@ object NotificationUtils {
|
|||||||
Channel(
|
Channel(
|
||||||
NOTIFICATION_CHANNEL_MESSAGES_V4,
|
NOTIFICATION_CHANNEL_MESSAGES_V4,
|
||||||
context.resources.getString(R.string.nc_notification_channel_messages),
|
context.resources.getString(R.string.nc_notification_channel_messages),
|
||||||
context.resources.getString(R.string.nc_notification_channel_messages_description)
|
context.resources.getString(R.string.nc_notification_channel_messages_description),
|
||||||
|
true
|
||||||
),
|
),
|
||||||
soundUri,
|
soundUri,
|
||||||
audioAttributes
|
audioAttributes
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun createUploadsNotificationChannel(
|
||||||
|
context: Context
|
||||||
|
) {
|
||||||
|
createNotificationChannel(
|
||||||
|
context,
|
||||||
|
Channel(
|
||||||
|
NOTIFICATION_CHANNEL_UPLOADS,
|
||||||
|
context.resources.getString(R.string.nc_notification_channel_uploads),
|
||||||
|
context.resources.getString(R.string.nc_notification_channel_uploads_description),
|
||||||
|
false
|
||||||
|
),
|
||||||
|
null,
|
||||||
|
null
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fun registerNotificationChannels(
|
fun registerNotificationChannels(
|
||||||
context: Context,
|
context: Context,
|
||||||
appPreferences: AppPreferences
|
appPreferences: AppPreferences
|
||||||
) {
|
) {
|
||||||
createCallsNotificationChannel(context, appPreferences)
|
createCallsNotificationChannel(context, appPreferences)
|
||||||
createMessagesNotificationChannel(context, appPreferences)
|
createMessagesNotificationChannel(context, appPreferences)
|
||||||
|
createUploadsNotificationChannel(context)
|
||||||
}
|
}
|
||||||
|
|
||||||
@TargetApi(Build.VERSION_CODES.O)
|
@TargetApi(Build.VERSION_CODES.O)
|
||||||
@ -327,6 +354,7 @@ object NotificationUtils {
|
|||||||
private data class Channel(
|
private data class Channel(
|
||||||
val id: String,
|
val id: String,
|
||||||
val name: String,
|
val name: String,
|
||||||
val description: String
|
val description: String,
|
||||||
|
val isImportant: Boolean
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,99 @@
|
|||||||
|
package com.nextcloud.talk.utils
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Nextcloud Talk application
|
||||||
|
* @author Marcel Hibbe
|
||||||
|
* @author David A. Velasco
|
||||||
|
* @author Chris Narkiewicz
|
||||||
|
* Copyright (C) 2021 Marcel Hibbe <dev@mhibbe.de>
|
||||||
|
* Copyright (C) 2016 ownCloud GmbH.
|
||||||
|
* Copyright (C) 2020 Chris Narkiewicz <hello@ezaquarii.com>
|
||||||
|
*
|
||||||
|
* This program is free software: you can redistribute it and/or modify
|
||||||
|
* it under the terms of the GNU General Public License as published by
|
||||||
|
* the Free Software Foundation, either version 3 of the License, or
|
||||||
|
* at your option) any later version.
|
||||||
|
*
|
||||||
|
* This program is distributed in the hope that it will be useful,
|
||||||
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
* GNU General Public License for more details.
|
||||||
|
*
|
||||||
|
* You should have received a copy of the GNU General Public License
|
||||||
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
*/
|
||||||
|
import com.nextcloud.talk.api.NcApi
|
||||||
|
import com.nextcloud.talk.data.user.model.User
|
||||||
|
import io.reactivex.Observable
|
||||||
|
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||||
|
import io.reactivex.schedulers.Schedulers
|
||||||
|
|
||||||
|
object RemoteFileUtils {
|
||||||
|
private val TAG = RemoteFileUtils::class.java.simpleName
|
||||||
|
|
||||||
|
fun getNewPathIfFileExists(
|
||||||
|
ncApi: NcApi,
|
||||||
|
currentUser: User,
|
||||||
|
remotePath: String
|
||||||
|
): String {
|
||||||
|
var finalPath = remotePath
|
||||||
|
val fileExists = doesFileExist(
|
||||||
|
ncApi,
|
||||||
|
currentUser,
|
||||||
|
remotePath,
|
||||||
|
).blockingFirst()
|
||||||
|
|
||||||
|
if (fileExists) {
|
||||||
|
finalPath = getFileNameWithoutCollision(
|
||||||
|
ncApi,
|
||||||
|
currentUser,
|
||||||
|
remotePath
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return finalPath
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun doesFileExist(
|
||||||
|
ncApi: NcApi,
|
||||||
|
currentUser: User,
|
||||||
|
remotePath: String
|
||||||
|
): Observable<Boolean> {
|
||||||
|
return ncApi.checkIfFileExists(
|
||||||
|
ApiUtils.getCredentials(currentUser.username, currentUser.token),
|
||||||
|
ApiUtils.getUrlForFileUpload(
|
||||||
|
currentUser.baseUrl,
|
||||||
|
currentUser.userId,
|
||||||
|
remotePath
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.observeOn(AndroidSchedulers.mainThread()).map { response ->
|
||||||
|
response.isSuccessful
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun getFileNameWithoutCollision(
|
||||||
|
ncApi: NcApi,
|
||||||
|
currentUser: User,
|
||||||
|
remotePath: String
|
||||||
|
): String {
|
||||||
|
val extPos = remotePath.lastIndexOf('.')
|
||||||
|
var suffix: String
|
||||||
|
var extension = ""
|
||||||
|
var remotePathWithoutExtension = ""
|
||||||
|
if (extPos >= 0) {
|
||||||
|
extension = remotePath.substring(extPos + 1)
|
||||||
|
remotePathWithoutExtension = remotePath.substring(0, extPos)
|
||||||
|
}
|
||||||
|
var count = 2
|
||||||
|
var exists: Boolean
|
||||||
|
var newPath: String
|
||||||
|
do {
|
||||||
|
suffix = " ($count)"
|
||||||
|
newPath = if (extPos >= 0) "$remotePathWithoutExtension$suffix.$extension" else remotePath + suffix
|
||||||
|
exists = doesFileExist(ncApi, currentUser, newPath).blockingFirst()
|
||||||
|
count++
|
||||||
|
} while (exists)
|
||||||
|
return newPath
|
||||||
|
}
|
||||||
|
}
|
@ -22,36 +22,8 @@
|
|||||||
|
|
||||||
package com.nextcloud.talk.utils
|
package com.nextcloud.talk.utils
|
||||||
|
|
||||||
import android.content.Context
|
|
||||||
import android.database.Cursor
|
|
||||||
import android.net.Uri
|
|
||||||
import android.provider.OpenableColumns
|
|
||||||
import android.util.Log
|
|
||||||
|
|
||||||
class UriUtils {
|
class UriUtils {
|
||||||
companion object {
|
companion object {
|
||||||
fun getFileName(uri: Uri, context: Context?): String {
|
|
||||||
var filename: String? = null
|
|
||||||
if (uri.scheme == "content" && context != null) {
|
|
||||||
val cursor: Cursor? = context.contentResolver.query(uri, null, null, null, null)
|
|
||||||
try {
|
|
||||||
if (cursor != null && cursor.moveToFirst()) {
|
|
||||||
filename = cursor.getString(cursor.getColumnIndexOrThrow(OpenableColumns.DISPLAY_NAME))
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor?.close()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (filename == null) {
|
|
||||||
Log.d("UriUtils", "failed to get DISPLAY_NAME from uri. using fallback.")
|
|
||||||
filename = uri.path
|
|
||||||
val lastIndexOfSlash = filename!!.lastIndexOf('/')
|
|
||||||
if (lastIndexOfSlash != -1) {
|
|
||||||
filename = filename.substring(lastIndexOfSlash + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
fun hasHttpProtocollPrefixed(uri: String): Boolean {
|
fun hasHttpProtocollPrefixed(uri: String): Boolean {
|
||||||
return uri.startsWith("http://") || uri.startsWith("https://")
|
return uri.startsWith("http://") || uri.startsWith("https://")
|
||||||
|
27
app/src/main/res/drawable/upload_white.xml
Normal file
27
app/src/main/res/drawable/upload_white.xml
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<!--
|
||||||
|
@author Google LLC
|
||||||
|
Copyright (C) 2018 Google LLC
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
||||||
|
-->
|
||||||
|
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:width="24dp"
|
||||||
|
android:height="24dp"
|
||||||
|
android:viewportWidth="24"
|
||||||
|
android:viewportHeight="24">
|
||||||
|
|
||||||
|
<path
|
||||||
|
android:fillColor="#ffffff"
|
||||||
|
android:strokeWidth="1"
|
||||||
|
android:pathData="M 9,16 V 10 H 5 l 7,-7 7,7 h -4 v 6 H 9 m -4,4 v -2 h 14 v 2 z"/>
|
||||||
|
</vector>
|
@ -219,8 +219,10 @@
|
|||||||
<string name="nc_notification_channel">%1$s on %2$s notification channel</string>
|
<string name="nc_notification_channel">%1$s on %2$s notification channel</string>
|
||||||
<string name="nc_notification_channel_calls">Calls</string>
|
<string name="nc_notification_channel_calls">Calls</string>
|
||||||
<string name="nc_notification_channel_messages">Messages</string>
|
<string name="nc_notification_channel_messages">Messages</string>
|
||||||
|
<string name="nc_notification_channel_uploads">Uploads</string>
|
||||||
<string name="nc_notification_channel_calls_description">Notify about incoming calls</string>
|
<string name="nc_notification_channel_calls_description">Notify about incoming calls</string>
|
||||||
<string name="nc_notification_channel_messages_description">Notify about incoming messages</string>
|
<string name="nc_notification_channel_messages_description">Notify about incoming messages</string>
|
||||||
|
<string name="nc_notification_channel_uploads_description">Notify about upload progress</string>
|
||||||
<string name="nc_notification_settings">Notification settings</string>
|
<string name="nc_notification_settings">Notification settings</string>
|
||||||
<string name="nc_plain_old_messages">Messages</string>
|
<string name="nc_plain_old_messages">Messages</string>
|
||||||
<string name="nc_notify_me_always">Always notify</string>
|
<string name="nc_notify_me_always">Always notify</string>
|
||||||
@ -426,6 +428,13 @@
|
|||||||
<string name="nc_upload_in_progess">Uploading</string>
|
<string name="nc_upload_in_progess">Uploading</string>
|
||||||
<string name="nc_upload_from_device">Upload from device</string>
|
<string name="nc_upload_from_device">Upload from device</string>
|
||||||
|
|
||||||
|
<string name="nc_upload_notification_text">%1$s to %2$s - %3$s\%%</string>
|
||||||
|
<string name="nc_upload_failed_notification_title">Failure</string>
|
||||||
|
<string name="nc_upload_failed_notification_text">Failed to upload %1$s</string>
|
||||||
|
|
||||||
|
<!-- Video -->
|
||||||
|
<string name="nc_video_filename">Video recording from %1$s</string>
|
||||||
|
|
||||||
<!-- location sharing -->
|
<!-- location sharing -->
|
||||||
<string name="nc_share_location">Share location</string>
|
<string name="nc_share_location">Share location</string>
|
||||||
<string name="nc_location_permission_required">location permission is required</string>
|
<string name="nc_location_permission_required">location permission is required</string>
|
||||||
@ -524,6 +533,7 @@
|
|||||||
<string name="take_photo_send">Send</string>
|
<string name="take_photo_send">Send</string>
|
||||||
<string name="take_photo_error_deleting_picture">Error taking picture</string>
|
<string name="take_photo_error_deleting_picture">Error taking picture</string>
|
||||||
<string name="take_photo_permission">Taking a photo is not possible without permissions</string>
|
<string name="take_photo_permission">Taking a photo is not possible without permissions</string>
|
||||||
|
<string name="camera_permission_granted">Camera permission granted. Please choose camera again.</string>
|
||||||
|
|
||||||
<!-- Audio selection -->
|
<!-- Audio selection -->
|
||||||
<string name="audio_output_bluetooth">Bluetooth</string>
|
<string name="audio_output_bluetooth">Bluetooth</string>
|
||||||
|
@ -1 +1 @@
|
|||||||
138
|
136
|
Loading…
Reference in New Issue
Block a user