mirror of
https://github.com/nextcloud/talk-android
synced 2025-03-06 06:15:12 +00:00
Merge pull request #2351 from nextcloud/feature/1587/directVideoUpload2
direct video upload + chunked upload
This commit is contained in:
commit
120219a711
@ -253,6 +253,9 @@
|
||||
<action android:name="android.intent.action.SEND" />
|
||||
<data android:mimeType="*/*" />
|
||||
</intent>
|
||||
<intent>
|
||||
<action android:name="android.media.action.VIDEO_CAPTURE" />
|
||||
</intent>
|
||||
</queries>
|
||||
|
||||
</manifest>
|
||||
|
@ -321,6 +321,7 @@ public class TakePhotoActivity extends AppCompatActivity {
|
||||
}
|
||||
});
|
||||
} 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();
|
||||
}
|
||||
});
|
||||
|
@ -65,6 +65,7 @@ import retrofit2.http.Field;
|
||||
import retrofit2.http.FieldMap;
|
||||
import retrofit2.http.FormUrlEncoded;
|
||||
import retrofit2.http.GET;
|
||||
import retrofit2.http.HEAD;
|
||||
import retrofit2.http.Header;
|
||||
import retrofit2.http.Multipart;
|
||||
import retrofit2.http.POST;
|
||||
@ -393,7 +394,7 @@ public interface NcApi {
|
||||
|
||||
@FormUrlEncoded
|
||||
@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("shareWith") String roomToken,
|
||||
@Field("shareType") String shareType,
|
||||
@ -420,6 +421,10 @@ public interface NcApi {
|
||||
@Url String url,
|
||||
@Body RequestBody body);
|
||||
|
||||
@HEAD
|
||||
Observable<Response<Void>> checkIfFileExists(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
||||
@GET
|
||||
Call<ResponseBody> downloadFile(@Header("Authorization") String authorization,
|
||||
@Url String url);
|
||||
|
@ -51,6 +51,7 @@ import android.os.SystemClock
|
||||
import android.os.VibrationEffect
|
||||
import android.os.Vibrator
|
||||
import android.provider.ContactsContract
|
||||
import android.provider.MediaStore
|
||||
import android.text.Editable
|
||||
import android.text.InputFilter
|
||||
import android.text.TextUtils
|
||||
@ -161,10 +162,10 @@ import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
||||
import com.nextcloud.talk.utils.ContactUtils
|
||||
import com.nextcloud.talk.utils.DateUtils
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.FileUtils
|
||||
import com.nextcloud.talk.utils.ImageEmojiEditText
|
||||
import com.nextcloud.talk.utils.MagicCharPolicy
|
||||
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.KEY_ACTIVE_CONVERSATION
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CONVERSATION_NAME
|
||||
@ -204,6 +205,7 @@ import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.Objects
|
||||
import java.util.concurrent.ExecutionException
|
||||
import javax.inject.Inject
|
||||
@ -284,6 +286,8 @@ class ChatController(args: Bundle) :
|
||||
|
||||
var hasChatPermission: Boolean = false
|
||||
|
||||
private var videoURI: Uri? = null
|
||||
|
||||
init {
|
||||
Log.d(TAG, "init ChatController: " + System.identityHashCode(this).toString())
|
||||
|
||||
@ -734,7 +738,7 @@ class ChatController(args: Bundle) :
|
||||
// Image keyboard support
|
||||
// See: https://developer.android.com/guide/topics/text/image-keyboard
|
||||
(binding.messageInputView.inputEditText as ImageEmojiEditText).onCommitContentListener = {
|
||||
uploadFiles(mutableListOf(it.toString()), false)
|
||||
uploadFile(it.toString(), false)
|
||||
}
|
||||
|
||||
showMicrophoneButton(true)
|
||||
@ -1089,8 +1093,7 @@ class ChatController(args: Bundle) :
|
||||
|
||||
@SuppressLint("SimpleDateFormat")
|
||||
private fun setVoiceRecordFileName() {
|
||||
val pattern = "yyyy-MM-dd HH-mm-ss"
|
||||
val simpleDateFormat = SimpleDateFormat(pattern)
|
||||
val simpleDateFormat = SimpleDateFormat(FILE_DATE_PATTERN)
|
||||
val date: String = simpleDateFormat.format(Date())
|
||||
|
||||
val fileNameWithoutSuffix = String.format(
|
||||
@ -1174,7 +1177,7 @@ class ChatController(args: Bundle) :
|
||||
private fun stopAndSendAudioRecording() {
|
||||
stopAudioRecording()
|
||||
val uri = Uri.fromFile(File(currentVoiceRecordFile))
|
||||
uploadFiles(mutableListOf(uri.toString()), true)
|
||||
uploadFile(uri.toString(), true)
|
||||
}
|
||||
|
||||
private fun stopAndDiscardAudioRecording() {
|
||||
@ -1360,6 +1363,7 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IllegalStateException::class)
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, intent: Intent?) {
|
||||
if (resultCode != RESULT_OK && (requestCode != REQUEST_CODE_MESSAGE_SEARCH)) {
|
||||
Log.e(TAG, "resultCode for received intent was != ok")
|
||||
@ -1404,7 +1408,7 @@ class ChatController(args: Bundle) :
|
||||
val filenamesWithLineBreaks = StringBuilder("\n")
|
||||
|
||||
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")
|
||||
}
|
||||
|
||||
@ -1422,7 +1426,7 @@ class ChatController(args: Bundle) :
|
||||
.setMessage(filenamesWithLineBreaks.toString())
|
||||
.setPositiveButton(R.string.nc_yes) { _, _ ->
|
||||
if (UploadAndShareFilesWorker.isStoragePermissionGranted(context)) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
uploadFiles(filesToUpload)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||
}
|
||||
@ -1467,25 +1471,31 @@ class ChatController(args: Bundle) :
|
||||
BuildConfig.APPLICATION_ID,
|
||||
File(file.absolutePath)
|
||||
)
|
||||
uploadFiles(mutableListOf(shareUri.toString()), false)
|
||||
uploadFile(shareUri.toString(), false)
|
||||
}
|
||||
cursor?.close()
|
||||
}
|
||||
REQUEST_CODE_PICK_CAMERA -> {
|
||||
if (resultCode == RESULT_OK) {
|
||||
try {
|
||||
checkNotNull(intent)
|
||||
filesToUpload.clear()
|
||||
run {
|
||||
checkNotNull(intent.data)
|
||||
intent.data.let {
|
||||
filesToUpload.add(intent.data.toString())
|
||||
|
||||
if (intent != null && intent.data != null) {
|
||||
run {
|
||||
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)) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
uploadFiles(filesToUpload)
|
||||
} else {
|
||||
UploadAndShareFilesWorker.requestStoragePermission(this)
|
||||
}
|
||||
@ -1548,7 +1558,7 @@ class ChatController(args: Bundle) :
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(ConversationsListController.TAG, "upload starting after permissions were granted")
|
||||
if (filesToUpload.isNotEmpty()) {
|
||||
uploadFiles(filesToUpload, false)
|
||||
uploadFiles(filesToUpload)
|
||||
}
|
||||
} else {
|
||||
Toast
|
||||
@ -1578,8 +1588,9 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
} else if (requestCode == REQUEST_CAMERA_PERMISSION) {
|
||||
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||
Log.d(TAG, "launch cam activity since permission for cam has been granted")
|
||||
startActivityForResult(TakePhotoActivity.createIntent(context), REQUEST_CODE_PICK_CAMERA)
|
||||
Toast
|
||||
.makeText(context, context.getString(R.string.camera_permission_granted), Toast.LENGTH_LONG)
|
||||
.show()
|
||||
} else {
|
||||
Toast
|
||||
.makeText(context, context.getString(R.string.take_photo_permission), Toast.LENGTH_LONG)
|
||||
@ -1588,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 = ""
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@ -1601,28 +1618,13 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
|
||||
try {
|
||||
require(files.isNotEmpty())
|
||||
val data: Data = Data.Builder()
|
||||
.putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, files.toTypedArray())
|
||||
.putString(
|
||||
UploadAndShareFilesWorker.NC_TARGETPATH,
|
||||
CapabilitiesUtilNew.getAttachmentFolder(conversationUser!!)
|
||||
)
|
||||
.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()
|
||||
}
|
||||
require(fileUri.isNotEmpty())
|
||||
UploadAndShareFilesWorker.upload(
|
||||
fileUri,
|
||||
roomToken!!,
|
||||
currentConversation?.displayName!!,
|
||||
metaData
|
||||
)
|
||||
} catch (e: IllegalArgumentException) {
|
||||
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)
|
||||
@ -2276,16 +2278,18 @@ class ChatController(args: Bundle) :
|
||||
val messagesToDelete: ArrayList<ChatMessage> = ArrayList()
|
||||
val systemTime = System.currentTimeMillis() / ONE_SECOND_IN_MILLIS
|
||||
|
||||
for (itemWrapper in adapter?.items!!) {
|
||||
if (itemWrapper.item is ChatMessage) {
|
||||
val chatMessage = itemWrapper.item as ChatMessage
|
||||
if (chatMessage.expirationTimestamp != 0 && chatMessage.expirationTimestamp < systemTime) {
|
||||
messagesToDelete.add(chatMessage)
|
||||
if (adapter?.items != null) {
|
||||
for (itemWrapper in adapter?.items!!) {
|
||||
if (itemWrapper.item is ChatMessage) {
|
||||
val chatMessage = itemWrapper.item as 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")) {
|
||||
@ -3253,6 +3257,36 @@ class ChatController(args: Bundle) :
|
||||
}
|
||||
}
|
||||
|
||||
fun sendVideoFromCamIntent() {
|
||||
if (!permissionUtil.isCameraPermissionGranted()) {
|
||||
requestCameraPermissions()
|
||||
} else {
|
||||
Intent(MediaStore.ACTION_VIDEO_CAPTURE).also { takeVideoIntent ->
|
||||
takeVideoIntent.resolveActivity(activity!!.packageManager)?.also {
|
||||
val videoFile: File? = try {
|
||||
val outputDir = context.cacheDir
|
||||
val dateFormat = SimpleDateFormat(FILE_DATE_PATTERN, Locale.ROOT)
|
||||
val date = dateFormat.format(Date())
|
||||
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 {
|
||||
videoURI = FileProvider.getUriForFile(context, context.packageName, it)
|
||||
takeVideoIntent.putExtra(MediaStore.EXTRA_OUTPUT, videoURI)
|
||||
startActivityForResult(takeVideoIntent, REQUEST_CODE_PICK_CAMERA)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun createPoll() {
|
||||
val pollVoteDialog = PollCreateDialogFragment.newInstance(
|
||||
roomToken!!
|
||||
@ -3288,6 +3322,8 @@ class ChatController(args: Bundle) :
|
||||
private const val VOICE_RECORD_CANCEL_SLIDER_X: Int = -50
|
||||
private const val VOICE_MESSAGE_META_DATA = "{\"messageType\":\"voice-message\"}"
|
||||
private const val VOICE_MESSAGE_FILE_SUFFIX = ".mp3"
|
||||
private const val FILE_DATE_PATTERN = "yyyy-MM-dd HH-mm-ss"
|
||||
private const val VIDEO_SUFFIX = ".mp4"
|
||||
private const val SHORT_VIBRATE: Long = 20
|
||||
private const val FULLY_OPAQUE_INT: Int = 255
|
||||
private const val SEMI_TRANSPARENT_INT: Int = 99
|
||||
|
@ -107,8 +107,8 @@ import com.nextcloud.talk.utils.AttendeePermissionsUtil
|
||||
import com.nextcloud.talk.utils.ClosedInterfaceImpl
|
||||
import com.nextcloud.talk.utils.ConductorRemapping.remapChatController
|
||||
import com.nextcloud.talk.utils.DisplayUtils
|
||||
import com.nextcloud.talk.utils.FileUtils
|
||||
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.KEY_ACTIVE_CONVERSATION
|
||||
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_SHARED_TEXT
|
||||
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.isServerEOL
|
||||
import com.nextcloud.talk.utils.database.user.CapabilitiesUtilNew.isUnifiedSearchAvailable
|
||||
@ -924,7 +923,7 @@ class ConversationsListController(bundle: Bundle) :
|
||||
if (isStoragePermissionGranted(context)) {
|
||||
val fileNamesWithLineBreaks = StringBuilder("\n")
|
||||
for (file in filesToShare!!) {
|
||||
val filename = getFileName(Uri.parse(file), context)
|
||||
val filename = FileUtils.getFileName(Uri.parse(file), context)
|
||||
fileNamesWithLineBreaks.append(filename).append("\n")
|
||||
}
|
||||
val confirmationQuestion: String = if (filesToShare!!.size == 1) {
|
||||
@ -1042,25 +1041,14 @@ class ConversationsListController(bundle: Bundle) :
|
||||
return
|
||||
}
|
||||
try {
|
||||
var filesToShareArray: Array<String?> = arrayOfNulls(filesToShare!!.size)
|
||||
filesToShareArray = filesToShare!!.toArray(filesToShareArray)
|
||||
val data = Data.Builder()
|
||||
.putStringArray(UploadAndShareFilesWorker.DEVICE_SOURCEFILES, filesToShareArray)
|
||||
.putString(
|
||||
UploadAndShareFilesWorker.NC_TARGETPATH,
|
||||
getAttachmentFolder(currentUser!!)
|
||||
filesToShare?.forEach {
|
||||
UploadAndShareFilesWorker.upload(
|
||||
it,
|
||||
selectedConversation!!.token!!,
|
||||
selectedConversation!!.displayName!!,
|
||||
null
|
||||
)
|
||||
.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) {
|
||||
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)
|
||||
|
@ -572,7 +572,7 @@ class ProfileController : BaseController(R.layout.controller_profile) {
|
||||
return null
|
||||
}
|
||||
|
||||
private fun createTempFileForAvatar(): File? {
|
||||
private fun createTempFileForAvatar(): File {
|
||||
FileUtils.removeTempCacheFile(
|
||||
this.context,
|
||||
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
|
||||
*
|
||||
* @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
|
||||
* it under the terms of the GNU General Public License as published by
|
||||
@ -21,46 +21,50 @@
|
||||
package com.nextcloud.talk.jobs
|
||||
|
||||
import android.Manifest
|
||||
import android.app.Notification
|
||||
import android.app.NotificationManager
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.os.SystemClock
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.core.content.PermissionChecker
|
||||
import androidx.work.Data
|
||||
import androidx.work.ExistingWorkPolicy
|
||||
import androidx.work.OneTimeWorkRequest
|
||||
import androidx.work.WorkManager
|
||||
import androidx.work.Worker
|
||||
import androidx.work.WorkerParameters
|
||||
import autodagger.AutoInjector
|
||||
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.application.NextcloudTalkApplication
|
||||
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.utils.ApiUtils
|
||||
import com.nextcloud.talk.utils.UriUtils
|
||||
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.FileUtils
|
||||
import com.nextcloud.talk.utils.NotificationUtils
|
||||
import com.nextcloud.talk.utils.RemoteFileUtils
|
||||
import com.nextcloud.talk.utils.bundle.BundleKeys.KEY_FROM_NOTIFICATION_START_CALL
|
||||
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 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.RequestBody
|
||||
import retrofit2.Response
|
||||
import java.io.File
|
||||
import java.io.FileNotFoundException
|
||||
import java.io.FileOutputStream
|
||||
import java.io.InputStream
|
||||
import okhttp3.OkHttpClient
|
||||
import javax.inject.Inject
|
||||
|
||||
@AutoInjector(NextcloudTalkApplication::class)
|
||||
class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerParameters) :
|
||||
Worker(context, workerParameters) {
|
||||
Worker(context, workerParameters), OnDataTransferProgressListener {
|
||||
|
||||
@Inject
|
||||
lateinit var ncApi: NcApi
|
||||
@ -71,6 +75,21 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
||||
@Inject
|
||||
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 {
|
||||
NextcloudTalkApplication.sharedApplication!!.componentApplication.inject(this)
|
||||
|
||||
@ -85,139 +104,173 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
||||
}
|
||||
|
||||
return try {
|
||||
val currentUser = userManager.currentUser.blockingGet()
|
||||
val sourcefiles = inputData.getStringArray(DEVICE_SOURCEFILES)
|
||||
val ncTargetpath = inputData.getString(NC_TARGETPATH)
|
||||
val roomToken = inputData.getString(ROOM_TOKEN)
|
||||
currentUser = userManager.currentUser.blockingGet()
|
||||
val sourceFile = inputData.getString(DEVICE_SOURCE_FILE)
|
||||
roomToken = inputData.getString(ROOM_TOKEN)!!
|
||||
conversationName = inputData.getString(CONVERSATION_NAME)!!
|
||||
val metaData = inputData.getString(META_DATA)
|
||||
|
||||
checkNotNull(currentUser)
|
||||
checkNotNull(sourcefiles)
|
||||
require(sourcefiles.isNotEmpty())
|
||||
checkNotNull(ncTargetpath)
|
||||
checkNotNull(sourceFile)
|
||||
require(sourceFile.isNotEmpty())
|
||||
checkNotNull(roomToken)
|
||||
|
||||
for (index in sourcefiles.indices) {
|
||||
val sourceFileUri = Uri.parse(sourcefiles[index])
|
||||
uploadFile(
|
||||
currentUser!!,
|
||||
UploadItem(
|
||||
sourceFileUri,
|
||||
UriUtils.getFileName(sourceFileUri, context),
|
||||
createRequestBody(sourceFileUri)
|
||||
),
|
||||
ncTargetpath,
|
||||
val sourceFileUri = Uri.parse(sourceFile)
|
||||
fileName = FileUtils.getFileName(sourceFileUri, context)
|
||||
val file = FileUtils.getFileFromUri(context, sourceFileUri)
|
||||
val remotePath = getRemotePath(currentUser)
|
||||
val uploadSuccess: Boolean
|
||||
|
||||
if (file != null && file.length() > CHUNK_UPLOAD_THRESHOLD_SIZE) {
|
||||
Log.d(TAG, "starting chunked upload because size is " + file.length())
|
||||
|
||||
initNotification()
|
||||
val mimeType = context.contentResolver.getType(sourceFileUri)?.toMediaTypeOrNull()
|
||||
|
||||
uploadSuccess = ChunkedFileUploader(
|
||||
okHttpClient!!,
|
||||
currentUser,
|
||||
roomToken,
|
||||
metaData
|
||||
metaData,
|
||||
this
|
||||
).upload(
|
||||
file,
|
||||
mimeType,
|
||||
remotePath
|
||||
)
|
||||
}
|
||||
Result.success()
|
||||
} 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()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "starting normal upload (not chunked)")
|
||||
|
||||
@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)
|
||||
uploadSuccess = FileUploader(
|
||||
context,
|
||||
currentUser,
|
||||
roomToken,
|
||||
ncApi
|
||||
).upload(
|
||||
sourceFileUri,
|
||||
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) {
|
||||
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(
|
||||
currentUser: User,
|
||||
uploadItem: UploadItem,
|
||||
ncTargetPath: String?,
|
||||
roomToken: String?,
|
||||
metaData: String?
|
||||
) {
|
||||
ncApi.uploadFile(
|
||||
ApiUtils.getCredentials(currentUser.username, currentUser.token),
|
||||
ApiUtils.getUrlForFileUpload(currentUser.baseUrl, currentUser.userId, ncTargetPath, uploadItem.fileName),
|
||||
uploadItem.requestBody
|
||||
private fun getRemotePath(currentUser: User): String {
|
||||
var remotePath = CapabilitiesUtilNew.getAttachmentFolder(currentUser)!! + "/" + fileName
|
||||
remotePath = RemoteFileUtils.getNewPathIfFileExists(
|
||||
ncApi,
|
||||
currentUser,
|
||||
remotePath
|
||||
)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.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)
|
||||
}
|
||||
})
|
||||
return remotePath
|
||||
}
|
||||
|
||||
private fun copyFileToCache(sourceFileUri: Uri, filename: String) {
|
||||
val cachedFile = File(context.cacheDir, filename)
|
||||
override fun onTransferProgress(
|
||||
percentage: Int
|
||||
) {
|
||||
notification = mBuilder!!
|
||||
.setProgress(HUNDRED_PERCENT, percentage, false)
|
||||
.setContentText(getNotificationContentText(percentage))
|
||||
.build()
|
||||
|
||||
if (cachedFile.exists()) {
|
||||
Log.d(TAG, "file is already in cache")
|
||||
mNotifyManager!!.notify(notificationId, notification)
|
||||
}
|
||||
|
||||
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 {
|
||||
val outputStream = FileOutputStream(cachedFile)
|
||||
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)
|
||||
}
|
||||
fileName
|
||||
}
|
||||
}
|
||||
|
||||
private fun shareFile(
|
||||
roomToken: String?,
|
||||
currentUser: User,
|
||||
ncTargetpath: String?,
|
||||
filename: String?,
|
||||
metaData: String?
|
||||
) {
|
||||
val paths: MutableList<String> = ArrayList()
|
||||
paths.add("$ncTargetpath/$filename")
|
||||
private fun getIntentToOpenConversation(): PendingIntent? {
|
||||
val bundle = Bundle()
|
||||
val intent = Intent(context, MainActivity::class.java)
|
||||
intent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
|
||||
|
||||
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)
|
||||
bundle.putString(KEY_ROOM_TOKEN, roomToken)
|
||||
bundle.putParcelable(KEY_USER_ENTITY, currentUser)
|
||||
bundle.putBoolean(KEY_FROM_NOTIFICATION_START_CALL, false)
|
||||
|
||||
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()
|
||||
val shareWorker = OneTimeWorkRequest.Builder(ShareOperationWorker::class.java)
|
||||
.setInputData(data)
|
||||
.build()
|
||||
WorkManager.getInstance().enqueue(shareWorker)
|
||||
|
||||
mNotifyManager!!.notify(notificationId, notification)
|
||||
}
|
||||
|
||||
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 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 {
|
||||
return when {
|
||||
@ -276,11 +329,23 @@ class UploadAndShareFilesWorker(val context: Context, workerParameters: WorkerPa
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private data class UploadItem(
|
||||
val uri: Uri,
|
||||
val fileName: String,
|
||||
val requestBody: RequestBody?
|
||||
)
|
||||
fun upload(
|
||||
fileUri: String,
|
||||
roomToken: String,
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -23,6 +23,7 @@
|
||||
package com.nextcloud.talk.ui.dialog
|
||||
|
||||
import android.app.Activity
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
@ -81,6 +82,10 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
|
||||
if (!CapabilitiesUtilNew.hasSpreedFeatureCapability(chatController.conversationUser, "talk-polls")) {
|
||||
dialogAttachmentBinding.menuAttachPoll.visibility = View.GONE
|
||||
}
|
||||
|
||||
if (!context.packageManager.hasSystemFeature(PackageManager.FEATURE_CAMERA_ANY)) {
|
||||
dialogAttachmentBinding.menuAttachVideoFromCam.visibility = View.GONE
|
||||
}
|
||||
}
|
||||
|
||||
private fun initItemsClickListeners() {
|
||||
@ -99,6 +104,11 @@ class AttachmentDialog(val activity: Activity, var chatController: ChatControlle
|
||||
dismiss()
|
||||
}
|
||||
|
||||
dialogAttachmentBinding.menuAttachVideoFromCam.setOnClickListener {
|
||||
chatController.sendVideoFromCamIntent()
|
||||
dismiss()
|
||||
}
|
||||
|
||||
dialogAttachmentBinding.menuAttachPoll.setOnClickListener {
|
||||
chatController.createPoll()
|
||||
dismiss()
|
||||
|
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";
|
||||
}
|
||||
|
||||
public static String getUrlForFileUpload(String baseUrl, String user, String attachmentFolder, String filename) {
|
||||
return baseUrl + "/remote.php/dav/files/" + user + attachmentFolder + "/" + filename;
|
||||
public static String getUrlForFileUpload(String baseUrl, String user, String remotePath) {
|
||||
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) {
|
||||
|
@ -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_CALLS_V4 = "NOTIFICATION_CHANNEL_CALLS_V4"
|
||||
const val NOTIFICATION_CHANNEL_UPLOADS = "NOTIFICATION_CHANNEL_UPLOADS"
|
||||
|
||||
const val DEFAULT_CALL_RINGTONE_URI =
|
||||
"android.resource://" + BuildConfig.APPLICATION_ID + "/raw/librem_by_feandesign_call"
|
||||
@ -75,7 +76,7 @@ object NotificationUtils {
|
||||
context: Context,
|
||||
notificationChannel: Channel,
|
||||
sound: Uri?,
|
||||
audioAttributes: AudioAttributes
|
||||
audioAttributes: AudioAttributes?
|
||||
) {
|
||||
|
||||
val notificationManager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
@ -84,9 +85,16 @@ object NotificationUtils {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
|
||||
notificationManager.getNotificationChannel(notificationChannel.id) == null
|
||||
) {
|
||||
val channel = NotificationChannel(
|
||||
notificationChannel.id, notificationChannel.name,
|
||||
val importance = if (notificationChannel.isImportant) {
|
||||
NotificationManager.IMPORTANCE_HIGH
|
||||
} else {
|
||||
NotificationManager.IMPORTANCE_LOW
|
||||
}
|
||||
|
||||
val channel = NotificationChannel(
|
||||
notificationChannel.id,
|
||||
notificationChannel.name,
|
||||
importance
|
||||
)
|
||||
|
||||
channel.description = notificationChannel.description
|
||||
@ -115,7 +123,8 @@ object NotificationUtils {
|
||||
Channel(
|
||||
NOTIFICATION_CHANNEL_CALLS_V4,
|
||||
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,
|
||||
audioAttributes
|
||||
@ -138,19 +147,37 @@ object NotificationUtils {
|
||||
Channel(
|
||||
NOTIFICATION_CHANNEL_MESSAGES_V4,
|
||||
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,
|
||||
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(
|
||||
context: Context,
|
||||
appPreferences: AppPreferences
|
||||
) {
|
||||
createCallsNotificationChannel(context, appPreferences)
|
||||
createMessagesNotificationChannel(context, appPreferences)
|
||||
createUploadsNotificationChannel(context)
|
||||
}
|
||||
|
||||
@TargetApi(Build.VERSION_CODES.O)
|
||||
@ -327,6 +354,7 @@ object NotificationUtils {
|
||||
private data class Channel(
|
||||
val id: 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
|
||||
|
||||
import android.content.Context
|
||||
import android.database.Cursor
|
||||
import android.net.Uri
|
||||
import android.provider.OpenableColumns
|
||||
import android.util.Log
|
||||
|
||||
class UriUtils {
|
||||
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 {
|
||||
return uri.startsWith("http://") || uri.startsWith("https://")
|
||||
|
5
app/src/main/res/drawable/ic_baseline_videocam_24.xml
Normal file
5
app/src/main/res/drawable/ic_baseline_videocam_24.xml
Normal file
@ -0,0 +1,5 @@
|
||||
<vector android:height="24dp" android:tint="#000000"
|
||||
android:viewportHeight="24" android:viewportWidth="24"
|
||||
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
|
||||
<path android:fillColor="@android:color/white" android:pathData="M17,10.5V7c0,-0.55 -0.45,-1 -1,-1H4c-0.55,0 -1,0.45 -1,1v10c0,0.55 0.45,1 1,1h12c0.55,0 1,-0.45 1,-1v-3.5l4,4v-11l-4,4z"/>
|
||||
</vector>
|
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>
|
@ -171,6 +171,39 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menu_attach_video_from_cam"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="@dimen/bottom_sheet_item_height"
|
||||
android:background="?android:attr/selectableItemBackground"
|
||||
android:gravity="center_vertical"
|
||||
android:orientation="horizontal"
|
||||
android:paddingStart="@dimen/standard_padding"
|
||||
android:paddingEnd="@dimen/standard_padding"
|
||||
tools:ignore="UseCompoundDrawables">
|
||||
|
||||
<ImageView
|
||||
android:id="@+id/menu_icon_attach_video_from_cam"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:contentDescription="@null"
|
||||
android:src="@drawable/ic_baseline_videocam_24"
|
||||
app:tint="@color/high_emphasis_menu_icon" />
|
||||
|
||||
<androidx.appcompat.widget.AppCompatTextView
|
||||
android:id="@+id/txt_attach_video_from_cam"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="start|center_vertical"
|
||||
android:paddingStart="@dimen/standard_double_padding"
|
||||
android:paddingEnd="@dimen/zero"
|
||||
android:text="@string/nc_upload_video_from_cam"
|
||||
android:textAlignment="viewStart"
|
||||
android:textColor="@color/high_emphasis_text"
|
||||
android:textSize="@dimen/bottom_sheet_text_size" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<LinearLayout
|
||||
android:id="@+id/menu_attach_file_from_local"
|
||||
android:layout_width="match_parent"
|
||||
|
@ -219,8 +219,10 @@
|
||||
<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_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_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_plain_old_messages">Messages</string>
|
||||
<string name="nc_notify_me_always">Always notify</string>
|
||||
@ -416,6 +418,7 @@
|
||||
<!-- Upload -->
|
||||
<string name="nc_add_file">Add to conversation</string>
|
||||
<string name="nc_upload_picture_from_cam">Take photo</string>
|
||||
<string name="nc_upload_video_from_cam">Take video</string>
|
||||
<string name="nc_create_poll">Create poll</string>
|
||||
<string name="nc_upload_from_cloud">Share from %1$s</string>
|
||||
<string name="nc_upload_failed">Sorry, upload failed</string>
|
||||
@ -425,6 +428,13 @@
|
||||
<string name="nc_upload_in_progess">Uploading</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 -->
|
||||
<string name="nc_share_location">Share location</string>
|
||||
<string name="nc_location_permission_required">location permission is required</string>
|
||||
@ -523,6 +533,7 @@
|
||||
<string name="take_photo_send">Send</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="camera_permission_granted">Camera permission granted. Please choose camera again.</string>
|
||||
|
||||
<!-- Audio selection -->
|
||||
<string name="audio_output_bluetooth">Bluetooth</string>
|
||||
|
@ -1 +1 @@
|
||||
138
|
||||
136
|
Loading…
Reference in New Issue
Block a user