From 1f03f96d7a498eaa583a07d8f840590c979533e1 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Fri, 10 Jun 2022 23:41:29 +0200 Subject: [PATCH 1/5] Rudimentary ffmpeg thumbnail --- app/build.gradle | 6 +- app/src/main/AndroidManifest.xml | 5 ++ .../app/postCreation/PostCreationActivity.kt | 23 ++++---- .../photoEdit/VideoEditActivity.kt | 57 +++++++++++++++++++ .../main/res/layout/activity_video_edit.xml | 15 +++++ app/src/main/res/values/strings.xml | 1 + build.gradle | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- 8 files changed, 95 insertions(+), 16 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt create mode 100644 app/src/main/res/layout/activity_video_edit.xml diff --git a/app/build.gradle b/app/build.gradle index d54e1eaf..e09ea30c 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -133,7 +133,7 @@ dependencies { // Use the most recent version of CameraX - def cameraX_version = '1.1.0-rc01' + def cameraX_version = '1.1.0-rc02' implementation "androidx.camera:camera-core:$cameraX_version" implementation "androidx.camera:camera-camera2:$cameraX_version" // CameraX Lifecycle library @@ -142,8 +142,6 @@ dependencies { // CameraX View class implementation "androidx.camera:camera-view:$cameraX_version" - implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' - def room_version = "2.4.2" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" @@ -155,6 +153,8 @@ dependencies { * ---------------------------------------------------------- */ + implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' + implementation 'com.arthenica:ffmpeg-kit-full:4.5.1-1.LTS' implementation 'com.google.android.material:material:1.6.1' diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4a3bdf48..89f1821d 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -26,6 +26,11 @@ android:name=".posts.AlbumActivity" android:exported="false" android:theme="@style/AppTheme.ActionBar.Transparent"/> + + } - }.show() - } else { - val intent = Intent(this, PhotoEditActivity::class.java) - .putExtra(PhotoEditActivity.PICTURE_URI, photoData[position].imageUri) - .putExtra(PhotoEditActivity.PICTURE_POSITION, position) - editResultContract.launch(intent) - } + val intent = Intent( + this, + if(photoData[position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java + ) + .putExtra(PhotoEditActivity.PICTURE_URI, photoData[position].imageUri) + .putExtra(PhotoEditActivity.PICTURE_POSITION, position) + + editResultContract.launch(intent) + } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt new file mode 100644 index 00000000..53c3240d --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt @@ -0,0 +1,57 @@ +package org.pixeldroid.app.postCreation.photoEdit + +import android.net.Uri +import android.os.Bundle +import android.util.Log +import androidx.core.net.toUri +import com.arthenica.ffmpegkit.* +import com.arthenica.ffmpegkit.MediaInformation.KEY_DURATION +import com.bumptech.glide.Glide +import org.pixeldroid.app.databinding.ActivityVideoEditBinding +import org.pixeldroid.app.utils.BaseActivity +import java.io.File + + +class VideoEditActivity : BaseActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val binding = ActivityVideoEditBinding.inflate(layoutInflater) + + setContentView(binding.root) + val uri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI) as Uri? + val videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1) + + val inputVideoPath =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString() + val inputVideoPath2 =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString() + val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation + + val duration: Long? = mediaInformation?.getNumberProperty(KEY_DURATION) + + val file = File.createTempFile("temp_img", ".png").toUri() + + val outputImagePath =if(file.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForWrite(this, file) else file.toString() + + val session = FFmpegKit.execute( + "-i $inputVideoPath2 -filter_complex \"select='not(mod(n,1000))',scale=240:-1,tile=layout=4x1\" -vframes 1 -q:v 2 -y $outputImagePath" + ) + if (ReturnCode.isSuccess(session.returnCode)) { + Glide.with(this).load(file).into(binding.thumbnails) + // SUCCESS + } else if (ReturnCode.isCancel(session.returnCode)) { + + // CANCEL + } else { + + // FAILURE + Log.d("VideoEditActivity", + String.format("Command failed with state %s and rc %s.%s", + session.state, + session.returnCode, + session.failStackTrace)) + } + + } + companion object { + const val VIDEO_TAG = "VideoEditTag" + } +} \ No newline at end of file diff --git a/app/src/main/res/layout/activity_video_edit.xml b/app/src/main/res/layout/activity_video_edit.xml new file mode 100644 index 00000000..d7c9300f --- /dev/null +++ b/app/src/main/res/layout/activity_video_edit.xml @@ -0,0 +1,15 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 885c39c2..0e756cb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -262,4 +262,5 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail Play video Video editing is not yet supported + Reel showing thumbnails of the video you are editing \ No newline at end of file diff --git a/build.gradle b/build.gradle index 5d5d8d52..c2731152 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { mavenCentral() } dependencies { - classpath 'com.android.tools.build:gradle:7.1.3' + classpath 'com.android.tools.build:gradle:7.2.1' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 39b9af83..02b00d99 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ #Tue Jun 07 20:42:16 CEST 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME From 8cecfa3de6e23626555ade3208db73339775bec4 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sat, 18 Jun 2022 22:21:19 +0200 Subject: [PATCH 2/5] Video edit --- app/src/main/AndroidManifest.xml | 3 +- .../app/postCreation/PostCreationActivity.kt | 199 ++++++++++-- .../app/postCreation/PostCreationViewModel.kt | 30 ++ .../app/postCreation/carousel/CarouselItem.kt | 3 +- .../postCreation/carousel/ImageCarousel.kt | 66 ++-- .../photoEdit/PhotoEditActivity.kt | 6 +- .../photoEdit/VideoEditActivity.kt | 282 ++++++++++++++++-- .../java/org/pixeldroid/app/utils/Utils.kt | 9 +- app/src/main/res/drawable/double_circle.xml | 11 + app/src/main/res/drawable/error.xml | 5 + app/src/main/res/drawable/selector_mute.xml | 9 + app/src/main/res/drawable/thumb_left.xml | 25 ++ app/src/main/res/drawable/thumb_right.xml | 29 ++ app/src/main/res/drawable/volume_off.xml | 5 + app/src/main/res/drawable/volume_up.xml | 5 + .../main/res/layout/activity_video_edit.xml | 105 ++++++- app/src/main/res/layout/image_carousel.xml | 26 ++ .../{edit_photo_menu.xml => edit_menu.xml} | 4 +- app/src/main/res/values/strings.xml | 7 + gradle/wrapper/gradle-wrapper.properties | 2 +- 20 files changed, 744 insertions(+), 87 deletions(-) create mode 100644 app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt create mode 100644 app/src/main/res/drawable/double_circle.xml create mode 100644 app/src/main/res/drawable/error.xml create mode 100644 app/src/main/res/drawable/selector_mute.xml create mode 100644 app/src/main/res/drawable/thumb_left.xml create mode 100644 app/src/main/res/drawable/thumb_right.xml create mode 100644 app/src/main/res/drawable/volume_off.xml create mode 100644 app/src/main/res/drawable/volume_up.xml rename app/src/main/res/menu/{edit_photo_menu.xml => edit_menu.xml} (83%) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 89f1821d..4ba866d4 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -28,8 +28,7 @@ android:theme="@style/AppTheme.ActionBar.Transparent"/> + android:exported="false"/> = mutableMapOf() + // Keep track of temporary files to delete them (avoids filling cache super fast with videos) + private val tempFiles: ArrayList = ArrayList() + + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) binding = ActivityPostCreationBinding.inflate(layoutInflater) setContentView(binding.root) + + + user = db.userDao().getActiveUser() instance = user?.run { @@ -89,7 +108,7 @@ class PostCreationActivity : BaseActivity() { intent.clipData?.let { addPossibleImages(it) } val carousel: ImageCarousel = binding.carousel - carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video) }) + carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video, encodeProgress = null) }) carousel.layoutCarouselCallback = { if(it){ // Became a carousel @@ -109,7 +128,7 @@ class PostCreationActivity : BaseActivity() { // get the description and send the post binding.postCreationSendButton.setOnClickListener { - if (validateDescription() && photoData.isNotEmpty()) upload() + if (validatePost() && photoData.isNotEmpty()) upload() } // Button to retry image upload when it fails @@ -123,7 +142,7 @@ class PostCreationActivity : BaseActivity() { } binding.editPhotoButton.setOnClickListener { - carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition -> + carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> edit(currentPosition) } } @@ -133,27 +152,36 @@ class PostCreationActivity : BaseActivity() { } binding.savePhotoButton.setOnClickListener { - carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition -> + carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> savePicture(it, currentPosition) } } binding.removePhotoButton.setOnClickListener { - carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition -> + carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> photoData.removeAt(currentPosition) - carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) }) + sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) } + carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) }) binding.addPhotoButton.isEnabled = true } } } + override fun onDestroy() { + super.onDestroy() + FFmpegKit.cancel() + tempFiles.forEach { + it.delete() + } + } + /** * Will add as many images as possible to [photoData], from the [clipData], and if - * ([photoData].size + [clipData].itemCount) > [albumLimit] then it will only add as many images + * ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images * as are legal (if any) and a dialog will be shown to the user alerting them of this fact. */ - private fun addPossibleImages(clipData: ClipData){ + private fun addPossibleImages(clipData: ClipData) { var count = clipData.itemCount if(count + photoData.size > instance.albumLimit){ AlertDialog.Builder(this).apply { @@ -168,7 +196,7 @@ class PostCreationActivity : BaseActivity() { } for (i in 0 until count) { clipData.getItemAt(i).uri.let { - val sizeAndVideoPair: Pair = it.getSizeAndVideoValidate() + val sizeAndVideoPair: Pair = it.getSizeAndVideoValidate(photoData.size + 1) photoData.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second)) } } @@ -178,7 +206,7 @@ class PostCreationActivity : BaseActivity() { * Returns the size of the file of the Uri, and whether it is a video, * and opens a dialog in case it is too big or in case the file is unsupported. */ - private fun Uri.getSizeAndVideoValidate(): Pair { + private fun Uri.getSizeAndVideoValidate(editPosition: Int): Pair { val size: Long = if (toString().startsWith("content")) { contentResolver.query(this, null, null, null, null) @@ -209,7 +237,7 @@ class PostCreationActivity : BaseActivity() { if (sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) { val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize AlertDialog.Builder(this@PostCreationActivity).apply { - setMessage(getString(R.string.size_exceeds_instance_limit, photoData.size + 1, sizeInkBytes, maxSize)) + setMessage(getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)) setNegativeButton(android.R.string.ok) { _, _ -> } }.show() } @@ -221,7 +249,7 @@ class PostCreationActivity : BaseActivity() { result.data?.clipData?.let { addPossibleImages(it) } - binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) }) + binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) }) } else if (result.resultCode != Activity.RESULT_CANCELED) { Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show() } @@ -294,7 +322,7 @@ class PostCreationActivity : BaseActivity() { } - private fun validateDescription(): Boolean { + private fun validatePost(): Boolean { binding.postTextInputLayout.run { val content = editText?.length() ?: 0 if (content > counterMaxLength) { @@ -303,6 +331,13 @@ class PostCreationActivity : BaseActivity() { return false } } + if(!photoData.all { it.videoEncodeProgress == null }){ + AlertDialog.Builder(this).apply { + setMessage(R.string.still_encoding) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + return false + } return true } @@ -435,20 +470,132 @@ class PostCreationActivity : BaseActivity() { if (result?.resultCode == Activity.RESULT_OK && result.data != null) { val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0) photoData.getOrNull(position)?.apply { - imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri() - val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate() - size = imageSize - video = imageVideo + if (video) { + val muted: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MUTED, false) + val videoStart: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let { + if(it == -1f) null else it + } + val modified: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MODIFIED, false) + val videoEnd: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let { + if(it == -1f) null else it + } + if(modified){ + videoEncodeProgress = 0 + sessionMap[position]?.let { FFmpegKit.cancel(it) } + startEncoding(position, muted, videoStart, videoEnd) + } + } else { + imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri() + val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate(position) + size = imageSize + video = imageVideo + } progress = null uploadId = null } ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() - binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) }) + binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) }) } else if(result?.resultCode != Activity.RESULT_CANCELED){ Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() } } + /** + * @param muted should audio tracks be removed in the output + * @param videoStart when we want to start the video, in seconds, or null if we + * don't want to remove the start + * @param videoEnd when we want to end the video, in seconds, or null if we + * don't want to remove the end + */ + private fun startEncoding(position: Int, muted: Boolean, videoStart: Float?, videoEnd: Float?) { + val originalUri = photoData[position].imageUri + + // Having a meaningful suffix is necessary so that ffmpeg knows what to put in output + val suffix = if(originalUri.scheme == "content") { + contentResolver.getType(photoData[position].imageUri)?.takeLastWhile { it != '/' } + } else { + originalUri.toString().takeLastWhile { it != '/' } + } + val file = File.createTempFile("temp_video", ".$suffix") + //val file = File.createTempFile("temp_video", ".webm") + tempFiles.add(file) + val fileUri = file.toUri() + val outputVideoPath = ffmpegSafeUri(fileUri) + + val inputUri = photoData[position].imageUri + + val inputSafePath = ffmpegSafeUri(inputUri) + + val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(ffmpegSafeUri(inputUri)).mediaInformation + val totalVideoDuration = mediaInformation?.duration?.toFloatOrNull() + + val mutedString = if(muted) "-an" else "" + val startString = if(videoStart != null) "-ss $videoStart" else "" + + val endString = if(videoEnd != null) "-to ${videoEnd - (videoStart ?: 0f)}" else "" + + val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c copy $mutedString -y $outputVideoPath", + //val session: FFmpegSession = FFmpegKit.executeAsync("$startString -i $inputSafePath $endString -c:v libvpx-vp9 -c:a copy -an -y $outputVideoPath", + { session -> + val returnCode = session.returnCode + if (ReturnCode.isSuccess(returnCode)) { + fun successResult() { + // Hide progress indicator in carousel + binding.carousel.updateProgress(null, position, false) + val (imageSize, imageVideo) = outputVideoPath.toUri().let { + photoData[position].imageUri = it + it.getSizeAndVideoValidate(position) + } + photoData[position].videoEncodeProgress = null + photoData[position].size = imageSize + binding.carousel.addData(photoData.map { + CarouselItem(it.imageUri, + it.imageDescription, + it.video, + it.videoEncodeProgress) + }) + } + + val post = resultHandler.post { + successResult() + } + if(!post) { + Log.e(TAG, "Failed to post changes, trying to recover in 100ms") + resultHandler.postDelayed({successResult()}, 100) + } + Log.d(TAG, "Encode completed successfully in ${session.duration} milliseconds") + } else { + resultHandler.post { + binding.carousel.updateProgress(null, position, error = true) + photoData[position].videoEncodeProgress = null + } + Log.e(TAG, "Encode failed with state ${session.state} and rc $returnCode.${session.failStackTrace}") + } + }, + { log -> Log.d("PostCreationActivityEncoding", log.message) } + ) { statistics: Statistics? -> + + val timeInMilliseconds: Int? = statistics?.time + timeInMilliseconds?.let { + if (timeInMilliseconds > 0) { + val completePercentage = totalVideoDuration?.let { + val newTotalDuration = it - (videoStart ?: 0f) - (it - (videoEnd ?: it)) + timeInMilliseconds / (10*newTotalDuration) + } + resultHandler.post { + completePercentage?.let { + val rounded = it.roundToInt() + photoData[position].videoEncodeProgress = rounded + binding.carousel.updateProgress(rounded, position, false) + } + } + Log.d(TAG, "Encoding video: %$completePercentage.") + } + } + } + sessionMap[position] = session.sessionId + } + private fun edit(position: Int) { val intent = Intent( this, diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt new file mode 100644 index 00000000..8b7f1d1b --- /dev/null +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -0,0 +1,30 @@ +package org.pixeldroid.app.postCreation + +import android.content.ClipData +import android.os.Bundle +import androidx.lifecycle.LiveData +import androidx.lifecycle.MutableLiveData +import androidx.lifecycle.ViewModel +import androidx.lifecycle.ViewModelProvider + +class PostCreationViewModel : ViewModel() { + private val photoData: MutableLiveData> by lazy { + MutableLiveData>().also { + loadUsers() + } + } + + fun getUsers(): LiveData> { + return photoData + } + + private fun loadUsers() { + // Do an asynchronous operation to fetch users. + } +} +class PostCreationViewModelFactory(val bundle: ClipData? = null) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.getConstructor(ClipData::class.java).newInstance(bundle) + } + +} \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt index 37dfda86..bbd4b8c7 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/CarouselItem.kt @@ -5,5 +5,6 @@ import android.net.Uri data class CarouselItem constructor( val imageUrl: Uri, val caption: String? = null, - val video: Boolean + val video: Boolean, + var encodeProgress: Int? ) \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt index 5b628b3f..bb2473aa 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt @@ -69,7 +69,7 @@ class ImageCarousel( private var isBuiltInIndicator = false - private var data: List? = null + private var data: MutableList? = null var onItemClickListener: OnItemClickListener? = this set(value) { @@ -88,28 +88,34 @@ class ImageCarousel( /** * Get or set current item position */ - var currentPosition = -1 + var currentPosition = RecyclerView.NO_POSITION get() { return snapHelper.getSnapPosition(recyclerView.layoutManager) } set(value) { - val position = when { - value >= data?.size ?: 0 -> { - -1 - } - value < 0 -> { - -1 - } - else -> { - value - } + val position = when (value) { + !in 0..((data?.size?.minus(1)) ?: 0) -> RecyclerView.NO_POSITION + else -> value } - field = position + if (position != RecyclerView.NO_POSITION && field != position) { + val thisProgress = data?.get(position)?.encodeProgress + if (thisProgress != null) { + binding.encodeProgress.visibility = VISIBLE + binding.encodeInfoText.visibility = VISIBLE + binding.encodeInfoText.text = + context.getString(R.string.encode_progress).format(thisProgress) + binding.encodeProgress.progress = thisProgress + } else { + binding.encodeProgress.visibility = INVISIBLE + binding.encodeInfoText.visibility = INVISIBLE + } + } else binding.encodeProgress.visibility = INVISIBLE - if (position != -1) { + if (position != RecyclerView.NO_POSITION && recyclerView.scrollState == RecyclerView.SCROLL_STATE_IDLE) { recyclerView.smoothScrollToPosition(position) } + field = position } /** @@ -450,10 +456,9 @@ class ImageCarousel( private fun initListeners() { recyclerView.addOnScrollListener(object : RecyclerView.OnScrollListener() { override fun onScrolled(recyclerView: RecyclerView, dx: Int, dy: Int) { + val position = currentPosition if (showCaption) { - val position = snapHelper.getSnapPosition(recyclerView.layoutManager) - if (position >= 0) { val dataItem = adapter?.getItem(position) @@ -469,6 +474,8 @@ class ImageCarousel( } } + if(dx !=0 || dy != 0) currentPosition = position + onScrollListener?.onScrolled(recyclerView, dx, dy) } @@ -561,12 +568,37 @@ class ImageCarousel( adapter?.apply { addAll(data) - this@ImageCarousel.data = data + this@ImageCarousel.data = data.toMutableList() initOnScrollStateChange() } } + fun updateProgress(progress: Int?, position: Int, error: Boolean){ + data?.get(position)?.encodeProgress = progress + if(currentPosition == position) { + if (progress == null) { + binding.encodeProgress.visibility = INVISIBLE + binding.encodeInfoText.visibility = VISIBLE + if(error){ + binding.encodeInfoText.setText(R.string.encode_error) + binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.error), + null, null, null) + + } else { + binding.encodeInfoText.setText(R.string.encode_success) + binding.encodeInfoText.setCompoundDrawablesWithIntrinsicBounds(ContextCompat.getDrawable(context, R.drawable.check_circle_24), + null, null, null) + } + } else { + binding.encodeProgress.visibility = VISIBLE + binding.encodeProgress.progress = progress + binding.encodeInfoText.visibility = VISIBLE + binding.encodeInfoText.text = context.getString(R.string.encode_progress).format(progress) + } + } + } + /** * Goto previous item. */ diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt index d32789f9..04566a8b 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/PhotoEditActivity.kt @@ -148,7 +148,7 @@ class PhotoEditActivity : BaseActivity() { } override fun onCreateOptionsMenu(menu: Menu): Boolean { - menuInflater.inflate(R.menu.edit_photo_menu, menu) + menuInflater.inflate(R.menu.edit_menu, menu) return true } @@ -191,8 +191,8 @@ class PhotoEditActivity : BaseActivity() { } } - return super.onOptionsItemSelected(item) -} + return super.onOptionsItemSelected(item) + } fun onFilterSelected(filter: Filter) { filteredImage = compressedOriginalImage!!.copy(BITMAP_CONFIG, true) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt index 53c3240d..11d0526a 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt @@ -1,57 +1,279 @@ package org.pixeldroid.app.postCreation.photoEdit +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.media.AudioManager import android.net.Uri import android.os.Bundle +import android.os.Handler +import android.os.Looper +import android.text.format.DateUtils import android.util.Log +import android.view.Menu +import android.view.MenuItem +import android.view.View +import android.widget.FrameLayout +import android.widget.ImageView +import android.widget.SeekBar +import android.widget.SeekBar.OnSeekBarChangeListener import androidx.core.net.toUri +import androidx.core.os.HandlerCompat +import androidx.media.AudioAttributesCompat +import androidx.media2.common.MediaMetadata +import androidx.media2.common.UriMediaItem +import androidx.media2.player.MediaPlayer import com.arthenica.ffmpegkit.* -import com.arthenica.ffmpegkit.MediaInformation.KEY_DURATION import com.bumptech.glide.Glide +import com.google.android.material.slider.RangeSlider +import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityVideoEditBinding +import org.pixeldroid.app.postCreation.PostCreationActivity +import org.pixeldroid.app.postCreation.carousel.dpToPx import org.pixeldroid.app.utils.BaseActivity +import org.pixeldroid.app.utils.ffmpegSafeUri import java.io.File +import java.text.NumberFormat +import java.time.format.DateTimeFormatter +import java.util.* +import kotlin.collections.ArrayList class VideoEditActivity : BaseActivity() { + + private lateinit var mediaPlayer: MediaPlayer + private var videoPosition: Int = -1 + private lateinit var binding: ActivityVideoEditBinding + // Map photoData indexes to FFmpeg Session IDs + private val sessionList: ArrayList = arrayListOf() + private val tempFiles: ArrayList = ArrayList() + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - val binding = ActivityVideoEditBinding.inflate(layoutInflater) - + binding = ActivityVideoEditBinding.inflate(layoutInflater) setContentView(binding.root) - val uri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI) as Uri? - val videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1) - val inputVideoPath =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString() - val inputVideoPath2 =if(uri.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForRead(this, uri) else uri.toString() + supportActionBar?.setTitle(R.string.toolbar_title_edit) + supportActionBar?.setDisplayHomeAsUpEnabled(true) + supportActionBar?.setHomeButtonEnabled(true) + + + binding.videoRangeSeekBar.setCustomThumbDrawablesForValues(R.drawable.thumb_left,R.drawable.double_circle,R.drawable.thumb_right) + binding.videoRangeSeekBar.thumbRadius = 20.dpToPx(this) + + + val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper()) + + val uri = intent.getParcelableExtra(PhotoEditActivity.PICTURE_URI)!! + videoPosition = intent.getIntExtra(PhotoEditActivity.PICTURE_POSITION, -1) + + val inputVideoPath = ffmpegSafeUri(uri) val mediaInformation: MediaInformation? = FFprobeKit.getMediaInformation(inputVideoPath).mediaInformation - val duration: Long? = mediaInformation?.getNumberProperty(KEY_DURATION) - - val file = File.createTempFile("temp_img", ".png").toUri() - - val outputImagePath =if(file.toString().startsWith("content://")) FFmpegKitConfig.getSafParameterForWrite(this, file) else file.toString() - - val session = FFmpegKit.execute( - "-i $inputVideoPath2 -filter_complex \"select='not(mod(n,1000))',scale=240:-1,tile=layout=4x1\" -vframes 1 -q:v 2 -y $outputImagePath" - ) - if (ReturnCode.isSuccess(session.returnCode)) { - Glide.with(this).load(file).into(binding.thumbnails) - // SUCCESS - } else if (ReturnCode.isCancel(session.returnCode)) { - - // CANCEL - } else { - - // FAILURE - Log.d("VideoEditActivity", - String.format("Command failed with state %s and rc %s.%s", - session.state, - session.returnCode, - session.failStackTrace)) + binding.muter.setOnClickListener { + binding.muter.isSelected = !binding.muter.isSelected } + //Duration in seconds, or null + val duration: Float? = mediaInformation?.duration?.toFloatOrNull() + + binding.videoRangeSeekBar.valueFrom = 0f + binding.videoRangeSeekBar.valueTo = duration ?: 100f + binding.videoRangeSeekBar.values = listOf(0f,(duration?: 100f) / 2, duration ?: 100f) + + + val mediaItem: UriMediaItem = UriMediaItem.Builder(uri).build() + mediaItem.metadata = MediaMetadata.Builder() + .putString(MediaMetadata.METADATA_KEY_TITLE, "") + .build() + + mediaPlayer = MediaPlayer(this) + mediaPlayer.setMediaItem(mediaItem) + + //binding.videoView.mediaControlView?.setMediaController() + + // Configure audio + mediaPlayer.setAudioAttributes(AudioAttributesCompat.Builder() + .setLegacyStreamType(AudioManager.STREAM_MUSIC) + .setUsage(AudioAttributesCompat.USAGE_MEDIA) + .setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE) + .build() + ) + + findViewById(R.id.progress_bar)?.visibility = View.GONE + + mediaPlayer.prepare() + + binding.muter.setOnClickListener { + if(!binding.muter.isSelected) mediaPlayer.playerVolume = 0f + else mediaPlayer.playerVolume = 1f + binding.muter.isSelected = !binding.muter.isSelected + } + + binding.videoView.setPlayer(mediaPlayer) + + mediaPlayer.seekTo((binding.videoRangeSeekBar.values[1]*1000).toLong()) + + object : Runnable { + override fun run() { + val getCurrent = mediaPlayer.currentPosition / 1000f + if(getCurrent >= binding.videoRangeSeekBar.values[0] && getCurrent <= binding.videoRangeSeekBar.values[2] ) { + binding.videoRangeSeekBar.values = listOf(binding.videoRangeSeekBar.values[0],getCurrent, binding.videoRangeSeekBar.values[2]) + } + Handler(Looper.getMainLooper()).postDelayed(this, 1000) + } + }.run() + + binding.videoRangeSeekBar.addOnChangeListener { rangeSlider: RangeSlider, value, fromUser -> + // Responds to when the middle slider's value is changed + if(fromUser && value != rangeSlider.values[0] && value != rangeSlider.values[2]) { + mediaPlayer.seekTo((rangeSlider.values[1]*1000).toLong()) + } + } + + binding.videoRangeSeekBar.setLabelFormatter { value: Float -> + DateUtils.formatElapsedTime(value.toLong()) + } + + + val thumbInterval: Float? = duration?.div(7) + + thumbInterval?.let { + thumbnail(uri, resultHandler, binding.thumbnail1, it) + thumbnail(uri, resultHandler, binding.thumbnail2, it.times(2)) + thumbnail(uri, resultHandler, binding.thumbnail3, it.times(3)) + thumbnail(uri, resultHandler, binding.thumbnail4, it.times(4)) + thumbnail(uri, resultHandler, binding.thumbnail5, it.times(5)) + thumbnail(uri, resultHandler, binding.thumbnail6, it.times(6)) + thumbnail(uri, resultHandler, binding.thumbnail7, it.times(7)) + } + + } + + override fun onCreateOptionsMenu(menu: Menu): Boolean { + menuInflater.inflate(R.menu.edit_menu, menu) + return true + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + + when(item.itemId) { + android.R.id.home -> onBackPressed() + R.id.action_save -> { + returnWithValues() + } + R.id.action_reset -> { + resetControls() + } + } + + return super.onOptionsItemSelected(item) + } + + override fun onBackPressed() { + if (noEdits()) super.onBackPressed() + else { + val builder = AlertDialog.Builder(this) + builder.apply { + setMessage(R.string.save_before_returning) + setPositiveButton(android.R.string.ok) { _, _ -> + returnWithValues() + } + setNegativeButton(R.string.no_cancel_edit) { _, _ -> + super.onBackPressed() + } + } + // Create the AlertDialog + builder.show() + } + } + + private fun noEdits(): Boolean { + val videoPositions = binding.videoRangeSeekBar.values.let { + it[0] == 0f && it[2] == binding.videoRangeSeekBar.valueTo + } + val muted = binding.muter.isSelected + return !muted && videoPositions + } + + + private fun returnWithValues() { + val intent = Intent(this, PostCreationActivity::class.java) + .apply { + putExtra(PhotoEditActivity.PICTURE_POSITION, videoPosition) + putExtra(MUTED, binding.muter.isSelected) + putExtra(MODIFIED, !noEdits()) + putExtra(VIDEO_START, binding.videoRangeSeekBar.values.first()) + putExtra(VIDEO_END, binding.videoRangeSeekBar.values[2]) + addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT) + } + + setResult(Activity.RESULT_OK, intent) + finish() + } + + private fun resetControls() { + binding.videoRangeSeekBar.values = listOf(0f, binding.videoRangeSeekBar.valueTo/2, binding.videoRangeSeekBar.valueTo) + binding.muter.isSelected = false + } + + override fun onDestroy() { + super.onDestroy() + sessionList.forEach { + FFmpegKit.cancel(it) + } + tempFiles.forEach{ + it.delete() + } + mediaPlayer.close() + } + + private fun thumbnail( + inputUri: Uri?, + resultHandler: Handler, + thumbnail: ImageView, + thumbTime: Float, + ) { + val file = File.createTempFile("temp_img", ".bmp") + tempFiles.add(file) + val fileUri = file.toUri() + val inputSafePath = ffmpegSafeUri(inputUri) + + val outputImagePath = + if(fileUri.toString().startsWith("content://")) + FFmpegKitConfig.getSafParameterForWrite(this, fileUri) + else fileUri.toString() + val session = FFmpegKit.executeAsync( + "-noaccurate_seek -ss $thumbTime -i $inputSafePath -vf scale=${thumbnail.width}:${thumbnail.height} -frames:v 1 -f image2 -y $outputImagePath", + { session -> + val state = session.state + val returnCode = session.returnCode + + if (ReturnCode.isSuccess(returnCode)) { + // SUCCESS + resultHandler.post { + if(!this.isFinishing) + Glide.with(this).load(outputImagePath).centerCrop().into(thumbnail) + } + } + // CALLED WHEN SESSION IS EXECUTED + Log.d("VideoEditActivity", "FFmpeg process exited with state $state and rc $returnCode.${session.failStackTrace}") + }, + {/* CALLED WHEN SESSION PRINTS LOGS */ }) { /*CALLED WHEN SESSION GENERATES STATISTICS*/ } + sessionList.add(session.sessionId) + } + + override fun onPause() { + super.onPause() + mediaPlayer.pause() + } + companion object { const val VIDEO_TAG = "VideoEditTag" + const val MUTED = "VideoEditMutedTag" + const val VIDEO_START = "VideoEditVideoStartTag" + const val VIDEO_END = "VideoEditVideoEndTag" + const val MODIFIED = "VideoEditModifiedTag" } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index c724c15b..b2ef5187 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -19,6 +19,7 @@ import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView +import com.arthenica.ffmpegkit.FFmpegKitConfig import okhttp3.HttpUrl import org.pixeldroid.app.R import kotlin.properties.ReadWriteProperty @@ -65,6 +66,12 @@ fun normalizeDomain(domain: String): String { .trim(Char::isWhitespace) } +fun Context.ffmpegSafeUri(inputUri: Uri?): String = + if (inputUri?.scheme == "content") + FFmpegKitConfig.getSafParameterForRead(this, inputUri) + else inputUri.toString() + + fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { ImageDecoder @@ -107,7 +114,7 @@ fun Bitmap.flip(horizontal: Boolean, vertical: Boolean): Bitmap { return Bitmap.createBitmap(this, 0, 0, width, height, matrix, true) } -fun BaseActivity.openUrl(url: String): Boolean{ +fun BaseActivity.openUrl(url: String): Boolean { val intent = CustomTabsIntent.Builder().build() diff --git a/app/src/main/res/drawable/double_circle.xml b/app/src/main/res/drawable/double_circle.xml new file mode 100644 index 00000000..907d1235 --- /dev/null +++ b/app/src/main/res/drawable/double_circle.xml @@ -0,0 +1,11 @@ + + + + + diff --git a/app/src/main/res/drawable/error.xml b/app/src/main/res/drawable/error.xml new file mode 100644 index 00000000..17575711 --- /dev/null +++ b/app/src/main/res/drawable/error.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/selector_mute.xml b/app/src/main/res/drawable/selector_mute.xml new file mode 100644 index 00000000..7103cc0c --- /dev/null +++ b/app/src/main/res/drawable/selector_mute.xml @@ -0,0 +1,9 @@ + + + + + \ No newline at end of file diff --git a/app/src/main/res/drawable/thumb_left.xml b/app/src/main/res/drawable/thumb_left.xml new file mode 100644 index 00000000..d6c10419 --- /dev/null +++ b/app/src/main/res/drawable/thumb_left.xml @@ -0,0 +1,25 @@ + + + + + + + diff --git a/app/src/main/res/drawable/thumb_right.xml b/app/src/main/res/drawable/thumb_right.xml new file mode 100644 index 00000000..6b5f6222 --- /dev/null +++ b/app/src/main/res/drawable/thumb_right.xml @@ -0,0 +1,29 @@ + + + + + + + + + diff --git a/app/src/main/res/drawable/volume_off.xml b/app/src/main/res/drawable/volume_off.xml new file mode 100644 index 00000000..b71ede34 --- /dev/null +++ b/app/src/main/res/drawable/volume_off.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/drawable/volume_up.xml b/app/src/main/res/drawable/volume_up.xml new file mode 100644 index 00000000..836cad86 --- /dev/null +++ b/app/src/main/res/drawable/volume_up.xml @@ -0,0 +1,5 @@ + + + diff --git a/app/src/main/res/layout/activity_video_edit.xml b/app/src/main/res/layout/activity_video_edit.xml index d7c9300f..fe3feb98 100644 --- a/app/src/main/res/layout/activity_video_edit.xml +++ b/app/src/main/res/layout/activity_video_edit.xml @@ -1,15 +1,112 @@ + android:background="@android:color/black" + android:scrollbarThumbHorizontal="@drawable/thumb_left"> + + + + + + + + + app:layout_constraintLeft_toLeftOf="parent" + app:layout_constraintRight_toLeftOf="@+id/thumbnail2" /> + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/layout/image_carousel.xml b/app/src/main/res/layout/image_carousel.xml index 3bbd86fe..dee9a908 100644 --- a/app/src/main/res/layout/image_carousel.xml +++ b/app/src/main/res/layout/image_carousel.xml @@ -162,4 +162,30 @@ app:layout_constraintTop_toTopOf="@+id/indicator" tools:visibility="visible" /> + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/edit_photo_menu.xml b/app/src/main/res/menu/edit_menu.xml similarity index 83% rename from app/src/main/res/menu/edit_photo_menu.xml rename to app/src/main/res/menu/edit_menu.xml index 31194bd7..4730db3b 100644 --- a/app/src/main/res/menu/edit_photo_menu.xml +++ b/app/src/main/res/menu/edit_menu.xml @@ -7,13 +7,13 @@ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 0e756cb5..7a9f17e5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -263,4 +263,11 @@ For more info about Pixelfed, you can check here: https://pixelfed.org" Play video Video editing is not yet supported Reel showing thumbnails of the video you are editing + RESET + SAVE + Error encoding + Encode success! + Encode %1$d%% + Select what to keep of the video + One or more videos are still encoding. Wait for them to finish before uploading \ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 02b00d99..8e59e764 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -4,4 +4,4 @@ distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.3-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME -distributionSha256Sum=f581709a9c35e9cb92e16f585d2c4bc99b2b1a5f85d2badbd3dc6bff59e1e6dd +distributionSha256Sum=b586e04868a22fd817c8971330fec37e298f3242eb85c374181b12d637f80302 From 5c221e004ddb74401e720411ba6978c4e18cad20 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sun, 19 Jun 2022 13:02:05 +0200 Subject: [PATCH 3/5] huge refactor of PostCreation to use ViewModel --- app/src/main/AndroidManifest.xml | 4 +- .../app/postCreation/PostCreationActivity.kt | 386 ++++----------- .../app/postCreation/PostCreationViewModel.kt | 440 +++++++++++++++++- .../postCreation/carousel/ImageCarousel.kt | 37 +- .../photoEdit/VideoEditActivity.kt | 6 - .../app/utils/di/ApplicationComponent.kt | 2 + 6 files changed, 537 insertions(+), 338 deletions(-) diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 4ba866d4..81d102bb 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -44,9 +44,7 @@ + android:theme="@style/AppTheme.NoActionBar"> diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index 3e4890e9..63cd86af 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -7,7 +7,6 @@ import android.media.MediaScannerConnection import android.net.Uri import android.os.* import android.provider.MediaStore -import android.provider.OpenableColumns import android.util.Log import android.view.View import android.view.View.INVISIBLE @@ -20,14 +19,19 @@ import androidx.activity.viewModels import androidx.core.net.toFile import androidx.core.net.toUri import androidx.core.os.HandlerCompat -import androidx.lifecycle.Observer +import androidx.core.widget.addTextChangedListener +import androidx.core.widget.doAfterTextChanged +import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope +import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView import com.arthenica.ffmpegkit.* import com.google.android.material.snackbar.Snackbar import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers import io.reactivex.rxjava3.disposables.Disposable import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.collect +import kotlinx.coroutines.launch import okhttp3.MultipartBody import org.pixeldroid.app.MainActivity import org.pixeldroid.app.R @@ -48,16 +52,11 @@ import java.io.FileNotFoundException import java.io.IOException import java.io.OutputStream import java.text.SimpleDateFormat - -import kotlin.math.ceil -import com.arthenica.ffmpegkit.FFprobeKit import java.util.* -import kotlin.collections.ArrayList - import kotlin.math.roundToInt -private const val TAG = "Post Creation Activity" +const val TAG = "Post Creation Activity" data class PhotoData( var imageUri: Uri, @@ -74,16 +73,11 @@ class PostCreationActivity : BaseActivity() { private var user: UserDatabaseEntity? = null private lateinit var instance: InstanceDatabaseEntity - private val photoData: ArrayList = ArrayList() - private lateinit var binding: ActivityPostCreationBinding private val resultHandler: Handler = HandlerCompat.createAsync(Looper.getMainLooper()) - // Map photoData indexes to FFmpeg Session IDs - private val sessionMap: MutableMap = mutableMapOf() - // Keep track of temporary files to delete them (avoids filling cache super fast with videos) - private val tempFiles: ArrayList = ArrayList() + private lateinit var model: PostCreationViewModel override fun onCreate(savedInstanceState: Bundle?) { @@ -91,9 +85,6 @@ class PostCreationActivity : BaseActivity() { binding = ActivityPostCreationBinding.inflate(layoutInflater) setContentView(binding.root) - - - user = db.userDao().getActiveUser() instance = user?.run { @@ -102,47 +93,87 @@ class PostCreationActivity : BaseActivity() { } } ?: InstanceDatabaseEntity("", "") - binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars + val _model: PostCreationViewModel by viewModels { PostCreationViewModelFactory(application, intent.clipData!!, instance) } + model = _model - // get image URIs - intent.clipData?.let { addPossibleImages(it) } + model.getPhotoData().observe(this) { newPhotoData -> + // update UI + binding.carousel.addData( + newPhotoData.map { + CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) + } + ) + } - val carousel: ImageCarousel = binding.carousel - carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video, encodeProgress = null) }) - carousel.layoutCarouselCallback = { - if(it){ - // Became a carousel - binding.toolbarPostCreation.visibility = VISIBLE - } else { - // Became a grid - binding.toolbarPostCreation.visibility = INVISIBLE + lifecycleScope.launch { + repeatOnLifecycle(Lifecycle.State.STARTED) { + model.uiState.collect { uiState -> + uiState.userMessage?.let { + AlertDialog.Builder(binding.root.context).apply { + setMessage(it) + setNegativeButton(android.R.string.ok) { _, _ -> } + }.show() + + // Notify the ViewModel the message is displayed + model.userMessageShown() + } + binding.addPhotoButton.isEnabled = uiState.addPhotoButtonEnabled + enableButton(uiState.postCreationSendButtonEnabled) + binding.uploadProgressBar.visibility = if(uiState.uploadProgressBarVisible) VISIBLE else INVISIBLE + binding.uploadProgressBar.progress = uiState.uploadProgress + binding.uploadCompletedTextview.visibility = if(uiState.uploadCompletedTextviewVisible) VISIBLE else INVISIBLE + binding.removePhotoButton.isEnabled = uiState.removePhotoButtonEnabled + binding.editPhotoButton.isEnabled = uiState.editPhotoButtonEnabled + binding.uploadError.visibility = if(uiState.uploadErrorVisible) VISIBLE else INVISIBLE + binding.uploadErrorTextExplanation.visibility = if(uiState.uploadErrorExplanationVisible) VISIBLE else INVISIBLE + + binding.toolbarPostCreation.visibility = if(uiState.isCarousel) VISIBLE else INVISIBLE + binding.carousel.layoutCarousel = uiState.isCarousel + + + binding.uploadErrorTextExplanation.text = uiState.uploadErrorExplanationText + + uiState.newEncodingJobPosition?.let { position -> + uiState.newEncodingJobMuted?.let { muted -> + uiState.newEncodingJobVideoStart?.let { videoStart -> + uiState.newEncodingJobVideoEnd?.let { videoEnd -> + startEncoding(position, muted, videoStart, videoEnd) + model.encodingStarted() + } + } + } + } + } } } - carousel.maxEntries = instance.albumLimit - carousel.addPhotoButtonCallback = { - addPhoto() - } - carousel.updateDescriptionCallback = { position: Int, description: String -> - photoData.getOrNull(position)?.imageDescription = description + binding.newPostDescriptionInputField.doAfterTextChanged { + model.newPostDescriptionChanged(binding.newPostDescriptionInputField.text) } + binding.postTextInputLayout.counterMaxLength = instance.maxStatusChars + binding.carousel.apply { + layoutCarouselCallback = { model.becameCarousel(it)} + maxEntries = instance.albumLimit + addPhotoButtonCallback = { + addPhoto() + } + updateDescriptionCallback = { position: Int, description: String -> + model.updateDescription(position, description) + } + } // get the description and send the post binding.postCreationSendButton.setOnClickListener { - if (validatePost() && photoData.isNotEmpty()) upload() + if (validatePost() && model.isNotEmpty()) model.upload() } // Button to retry image upload when it fails binding.retryUploadButton.setOnClickListener { - binding.uploadError.visibility = View.GONE - photoData.forEach { - it.uploadId = null - it.progress = null - } - upload() + model.resetUploadStatus() + model.upload() } binding.editPhotoButton.setOnClickListener { - carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> + binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> edit(currentPosition) } } @@ -152,104 +183,25 @@ class PostCreationActivity : BaseActivity() { } binding.savePhotoButton.setOnClickListener { - carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> + binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> savePicture(it, currentPosition) } } binding.removePhotoButton.setOnClickListener { - carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> - photoData.removeAt(currentPosition) - sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) } - carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) }) - binding.addPhotoButton.isEnabled = true + binding.carousel.currentPosition.takeIf { it != RecyclerView.NO_POSITION }?.let { currentPosition -> + model.removeAt(currentPosition) + model.cancelEncode(currentPosition) } } } - override fun onDestroy() { - super.onDestroy() - FFmpegKit.cancel() - tempFiles.forEach { - it.delete() - } - } - - /** - * Will add as many images as possible to [photoData], from the [clipData], and if - * ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images - * as are legal (if any) and a dialog will be shown to the user alerting them of this fact. - */ - private fun addPossibleImages(clipData: ClipData) { - var count = clipData.itemCount - if(count + photoData.size > instance.albumLimit){ - AlertDialog.Builder(this).apply { - setMessage(getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) - setNegativeButton(android.R.string.ok) { _, _ -> } - }.show() - count = count.coerceAtMost(instance.albumLimit - photoData.size) - } - if (count + photoData.size >= instance.albumLimit) { - // Disable buttons to add more images - binding.addPhotoButton.isEnabled = false - } - for (i in 0 until count) { - clipData.getItemAt(i).uri.let { - val sizeAndVideoPair: Pair = it.getSizeAndVideoValidate(photoData.size + 1) - photoData.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second)) - } - } - } - - /** - * Returns the size of the file of the Uri, and whether it is a video, - * and opens a dialog in case it is too big or in case the file is unsupported. - */ - private fun Uri.getSizeAndVideoValidate(editPosition: Int): Pair { - val size: Long = - if (toString().startsWith("content")) { - contentResolver.query(this, null, null, null, null) - ?.use { cursor -> - /* Get the column indexes of the data in the Cursor, - * move to the first row in the Cursor, get the data, - * and display it. - */ - val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) - cursor.moveToFirst() - cursor.getLong(sizeIndex) - } ?: 0 - } else { - toFile().length() - } - - val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() - val type = contentResolver.getType(this) - val isVideo = type?.startsWith("video/") == true - - if(isVideo && !instance.videoEnabled){ - AlertDialog.Builder(this@PostCreationActivity).apply { - setMessage(R.string.video_not_supported) - setNegativeButton(android.R.string.ok) { _, _ -> } - }.show() - } - - if (sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) { - val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize - AlertDialog.Builder(this@PostCreationActivity).apply { - setMessage(getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize)) - setNegativeButton(android.R.string.ok) { _, _ -> } - }.show() - } - return Pair(size, isVideo) - } - private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> if (result.resultCode == Activity.RESULT_OK && result.data?.clipData != null) { result.data?.clipData?.let { - addPossibleImages(it) + model.setImages(model.addPossibleImages(it)) } - binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) }) } else if (result.resultCode != Activity.RESULT_CANCELED) { Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show() } @@ -268,7 +220,7 @@ class PostCreationActivity : BaseActivity() { val outputStream: OutputStream = pair.first val path: String = pair.second - contentResolver.openInputStream(photoData[currentPosition].imageUri)!!.use { input -> + contentResolver.openInputStream(model.getPhotoData().value!![currentPosition].imageUri)!!.use { input -> outputStream.use { output -> input.copyTo(output) } @@ -331,7 +283,7 @@ class PostCreationActivity : BaseActivity() { return false } } - if(!photoData.all { it.videoEncodeProgress == null }){ + if(model.getPhotoData().value?.all { it.videoEncodeProgress == null } == false){ AlertDialog.Builder(this).apply { setMessage(R.string.still_encoding) setNegativeButton(android.R.string.ok) { _, _ -> } @@ -341,118 +293,6 @@ class PostCreationActivity : BaseActivity() { return true } - /** - * Uploads the images that are in the [photoData] array. - * Keeps track of them in the [PhotoData.progress] (for the upload progress), and the - * [PhotoData.uploadId] (for the list of ids of the uploads). - */ - private fun upload() { - enableButton(false) - binding.uploadProgressBar.visibility = VISIBLE - binding.uploadCompletedTextview.visibility = INVISIBLE - binding.removePhotoButton.isEnabled = false - binding.editPhotoButton.isEnabled = false - binding.addPhotoButton.isEnabled = false - - for (data: PhotoData in photoData) { - val imageUri = data.imageUri - val imageInputStream = try { - contentResolver.openInputStream(imageUri)!! - } catch (e: FileNotFoundException){ - AlertDialog.Builder(this).apply { - setMessage(getString(R.string.file_not_found).format(imageUri)) - - setNegativeButton(android.R.string.ok) { _, _ -> } - }.show() - return - } - - val imagePart = ProgressRequestBody(imageInputStream, data.size) - val requestBody = MultipartBody.Builder() - .setType(MultipartBody.FORM) - .addFormDataPart("file", System.currentTimeMillis().toString(), imagePart) - .build() - - val sub = imagePart.progressSubject - .subscribeOn(Schedulers.io()) - .subscribe { percentage -> - data.progress = percentage.toInt() - binding.uploadProgressBar.progress = - photoData.sumOf { it.progress ?: 0 } / photoData.size - } - - var postSub: Disposable? = null - - val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) } - - val api = apiHolder.api ?: apiHolder.setToCurrentUser() - val inter = api.mediaUpload(description, requestBody.parts[0]) - - postSub = inter - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribe( - { attachment: Attachment -> - data.progress = 0 - data.uploadId = attachment.id!! - }, - { e: Throwable -> - binding.uploadError.visibility = View.VISIBLE - if(e is HttpException){ - binding.uploadErrorTextExplanation.text = - getString(R.string.upload_error, e.code()) - binding.uploadErrorTextExplanation.visibility= VISIBLE - } else { - binding.uploadErrorTextExplanation.visibility= View.GONE - } - e.printStackTrace() - postSub?.dispose() - sub.dispose() - }, - { - data.progress = 100 - if (photoData.all { it.progress == 100 && it.uploadId != null }) { - binding.uploadProgressBar.visibility = View.GONE - binding.uploadCompletedTextview.visibility = View.VISIBLE - post() - } - postSub?.dispose() - sub.dispose() - } - ) - } - } - - private fun post() { - val description = binding.newPostDescriptionInputField.text.toString() - enableButton(false) - lifecycleScope.launchWhenCreated { - try { - val api = apiHolder.api ?: apiHolder.setToCurrentUser() - - api.postStatus( - statusText = description, - media_ids = photoData.mapNotNull { it.uploadId }.toList() - ) - Toast.makeText(applicationContext, getString(R.string.upload_post_success), - Toast.LENGTH_SHORT).show() - val intent = Intent(this@PostCreationActivity, MainActivity::class.java) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK - startActivity(intent) - } catch (exception: IOException) { - Toast.makeText(applicationContext, getString(R.string.upload_post_error), - Toast.LENGTH_SHORT).show() - Log.e(TAG, exception.toString()) - enableButton(true) - } catch (exception: HttpException) { - Toast.makeText(applicationContext, getString(R.string.upload_post_failed), - Toast.LENGTH_SHORT).show() - Log.e(TAG, exception.response().toString() + exception.message().toString()) - enableButton(true) - } - } - } - private fun enableButton(enable: Boolean = true){ binding.postCreationSendButton.isEnabled = enable if(enable){ @@ -469,32 +309,8 @@ class PostCreationActivity : BaseActivity() { result: ActivityResult? -> if (result?.resultCode == Activity.RESULT_OK && result.data != null) { val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0) - photoData.getOrNull(position)?.apply { - if (video) { - val muted: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MUTED, false) - val videoStart: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let { - if(it == -1f) null else it - } - val modified: Boolean = result.data!!.getBooleanExtra(VideoEditActivity.MODIFIED, false) - val videoEnd: Float? = result.data!!.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let { - if(it == -1f) null else it - } - if(modified){ - videoEncodeProgress = 0 - sessionMap[position]?.let { FFmpegKit.cancel(it) } - startEncoding(position, muted, videoStart, videoEnd) - } - } else { - imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri() - val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate(position) - size = imageSize - video = imageVideo - } - progress = null - uploadId = null - } ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() - - binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video, it.videoEncodeProgress) }) + model.modifyAt(position, result.data!!) + ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() } else if(result?.resultCode != Activity.RESULT_CANCELED){ Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() } @@ -508,21 +324,21 @@ class PostCreationActivity : BaseActivity() { * don't want to remove the end */ private fun startEncoding(position: Int, muted: Boolean, videoStart: Float?, videoEnd: Float?) { - val originalUri = photoData[position].imageUri + val originalUri = model.getPhotoData().value!![position].imageUri // Having a meaningful suffix is necessary so that ffmpeg knows what to put in output val suffix = if(originalUri.scheme == "content") { - contentResolver.getType(photoData[position].imageUri)?.takeLastWhile { it != '/' } + contentResolver.getType(model.getPhotoData().value!![position].imageUri)?.takeLastWhile { it != '/' } } else { originalUri.toString().takeLastWhile { it != '/' } } val file = File.createTempFile("temp_video", ".$suffix") //val file = File.createTempFile("temp_video", ".webm") - tempFiles.add(file) + model.trackTempFile(file) val fileUri = file.toUri() val outputVideoPath = ffmpegSafeUri(fileUri) - val inputUri = photoData[position].imageUri + val inputUri = model.getPhotoData().value!![position].imageUri val inputSafePath = ffmpegSafeUri(inputUri) @@ -542,18 +358,12 @@ class PostCreationActivity : BaseActivity() { fun successResult() { // Hide progress indicator in carousel binding.carousel.updateProgress(null, position, false) - val (imageSize, imageVideo) = outputVideoPath.toUri().let { - photoData[position].imageUri = it - it.getSizeAndVideoValidate(position) + val (imageSize, _) = outputVideoPath.toUri().let { + model.setUriAtPosition(it, position) + model.getSizeAndVideoValidate(it, position) } - photoData[position].videoEncodeProgress = null - photoData[position].size = imageSize - binding.carousel.addData(photoData.map { - CarouselItem(it.imageUri, - it.imageDescription, - it.video, - it.videoEncodeProgress) - }) + model.setVideoEncodeAtPosition(position, null) + model.setSizeAtPosition(imageSize, position) } val post = resultHandler.post { @@ -567,7 +377,7 @@ class PostCreationActivity : BaseActivity() { } else { resultHandler.post { binding.carousel.updateProgress(null, position, error = true) - photoData[position].videoEncodeProgress = null + model.setVideoEncodeAtPosition(position, null) } Log.e(TAG, "Encode failed with state ${session.state} and rc $returnCode.${session.failStackTrace}") } @@ -584,8 +394,8 @@ class PostCreationActivity : BaseActivity() { } resultHandler.post { completePercentage?.let { - val rounded = it.roundToInt() - photoData[position].videoEncodeProgress = rounded + val rounded: Int = it.roundToInt() + model.setVideoEncodeAtPosition(position, rounded) binding.carousel.updateProgress(rounded, position, false) } } @@ -593,15 +403,15 @@ class PostCreationActivity : BaseActivity() { } } } - sessionMap[position] = session.sessionId + model.registerNewFFmpegSession(position, session.sessionId) } private fun edit(position: Int) { val intent = Intent( this, - if(photoData[position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java + if(model.getPhotoData().value!![position].video) VideoEditActivity::class.java else PhotoEditActivity::class.java ) - .putExtra(PhotoEditActivity.PICTURE_URI, photoData[position].imageUri) + .putExtra(PhotoEditActivity.PICTURE_URI, model.getPhotoData().value!![position].imageUri) .putExtra(PhotoEditActivity.PICTURE_POSITION, position) editResultContract.launch(intent) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt index 8b7f1d1b..6f4ad5be 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationViewModel.kt @@ -1,30 +1,436 @@ package org.pixeldroid.app.postCreation +import android.app.Application import android.content.ClipData -import android.os.Bundle -import androidx.lifecycle.LiveData -import androidx.lifecycle.MutableLiveData -import androidx.lifecycle.ViewModel -import androidx.lifecycle.ViewModelProvider +import android.content.Intent +import android.net.Uri +import android.provider.OpenableColumns +import android.text.Editable +import android.util.Log +import android.widget.Toast +import androidx.core.net.toFile +import androidx.core.net.toUri +import androidx.lifecycle.* +import com.arthenica.ffmpegkit.FFmpegKit +import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers +import io.reactivex.rxjava3.disposables.Disposable +import io.reactivex.rxjava3.schedulers.Schedulers +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import okhttp3.MultipartBody +import org.pixeldroid.app.MainActivity +import org.pixeldroid.app.R +import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity +import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity +import org.pixeldroid.app.utils.PixelDroidApplication +import org.pixeldroid.app.utils.api.objects.Attachment +import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity +import org.pixeldroid.app.utils.di.PixelfedAPIHolder +import retrofit2.HttpException +import java.io.File +import java.io.FileNotFoundException +import java.io.IOException +import javax.inject.Inject +import kotlin.math.ceil -class PostCreationViewModel : ViewModel() { - private val photoData: MutableLiveData> by lazy { - MutableLiveData>().also { - loadUsers() +// Models the UI state for the PostCreationActivity +data class PostCreationActivityUiState( + val userMessage: String? = null, + + val addPhotoButtonEnabled: Boolean = true, + val editPhotoButtonEnabled: Boolean = true, + val removePhotoButtonEnabled: Boolean = true, + val postCreationSendButtonEnabled: Boolean = true, + + val isCarousel: Boolean = true, + + val newPostDescriptionText: String = "", + + val uploadProgressBarVisible: Boolean = false, + val uploadProgress: Int = 0, + val uploadCompletedTextviewVisible: Boolean = false, + val uploadErrorVisible: Boolean = false, + val uploadErrorExplanationText: String = "", + val uploadErrorExplanationVisible: Boolean = false, + + val newEncodingJobPosition: Int? = null, + val newEncodingJobMuted: Boolean? = null, + val newEncodingJobVideoStart: Float? = null, + val newEncodingJobVideoEnd: Float? = null, +) + +class PostCreationViewModel(application: Application, clipdata: ClipData? = null, val instance: InstanceDatabaseEntity? = null) : AndroidViewModel(application) { + private val photoData: MutableLiveData> by lazy { + MutableLiveData>().also { + it.value = clipdata?.let { it1 -> addPossibleImages(it1, mutableListOf()) } } } - fun getUsers(): LiveData> { - return photoData + @Inject + lateinit var apiHolder: PixelfedAPIHolder + + init { + (application as PixelDroidApplication).getAppComponent().inject(this) } - private fun loadUsers() { - // Do an asynchronous operation to fetch users. + // Map photoData indexes to FFmpeg Session IDs + private val sessionMap: MutableMap = mutableMapOf() + // Keep track of temporary files to delete them (avoids filling cache super fast with videos) + private val tempFiles: java.util.ArrayList = java.util.ArrayList() + + + private val _uiState = MutableStateFlow(PostCreationActivityUiState()) + val uiState: StateFlow = _uiState + + fun userMessageShown() { + _uiState.update { currentUiState -> + currentUiState.copy(userMessage = null) + } } + + fun encodingStarted() { + _uiState.update { currentUiState -> + currentUiState.copy( + newEncodingJobPosition = null, + newEncodingJobMuted = null, + newEncodingJobVideoStart = null, + newEncodingJobVideoEnd = null, + ) + } + } + + fun getPhotoData(): LiveData> = photoData + + /** + * Will add as many images as possible to [photoData], from the [clipData], and if + * ([photoData].size + [clipData].itemCount) > [InstanceDatabaseEntity.albumLimit] then it will only add as many images + * as are legal (if any) and a dialog will be shown to the user alerting them of this fact. + */ + fun addPossibleImages(clipData: ClipData, previousList: MutableList? = photoData.value): MutableList { + val dataToAdd: ArrayList = arrayListOf() + var count = clipData.itemCount + if(count + (previousList?.size ?: 0) > instance!!.albumLimit){ + _uiState.update { currentUiState -> + currentUiState.copy(userMessage = getApplication().getString(R.string.total_exceeds_album_limit).format(instance.albumLimit)) + } + count = count.coerceAtMost(instance.albumLimit - (previousList?.size ?: 0)) + } + if (count + (previousList?.size ?: 0) >= instance.albumLimit) { + // Disable buttons to add more images + _uiState.update { currentUiState -> + currentUiState.copy(addPhotoButtonEnabled = false) + } + } + for (i in 0 until count) { + clipData.getItemAt(i).uri.let { + val sizeAndVideoPair: Pair = + getSizeAndVideoValidate(it, (previousList?.size ?: 0) + dataToAdd.size + 1) + dataToAdd.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second)) + } + } + return previousList?.plus(dataToAdd)?.toMutableList() ?: mutableListOf() + } + + fun setImages(addPossibleImages: MutableList) { + photoData.value = addPossibleImages + } + + /** + * Returns the size of the file of the Uri, and whether it is a video, + * and opens a dialog in case it is too big or in case the file is unsupported. + */ + fun getSizeAndVideoValidate(uri: Uri, editPosition: Int): Pair { + val size: Long = + if (uri.scheme =="content") { + getApplication().contentResolver.query(uri, null, null, null, null) + ?.use { cursor -> + /* Get the column indexes of the data in the Cursor, + * move to the first row in the Cursor, get the data, + * and display it. + */ + val sizeIndex = cursor.getColumnIndex(OpenableColumns.SIZE) + cursor.moveToFirst() + cursor.getLong(sizeIndex) + } ?: 0 + } else { + uri.toFile().length() + } + + val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() + val type = getApplication().contentResolver.getType(uri) + val isVideo = type?.startsWith("video/") == true + + if(isVideo && !instance!!.videoEnabled){ + _uiState.update { currentUiState -> + currentUiState.copy(userMessage = getApplication().getString(R.string.video_not_supported)) + } + } + + if (sizeInkBytes > instance!!.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) { + val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize + _uiState.update { currentUiState -> + currentUiState.copy( + userMessage = getApplication().getString(R.string.size_exceeds_instance_limit, editPosition, sizeInkBytes, maxSize) + ) + } + } + return Pair(size, isVideo) + } + + fun isNotEmpty(): Boolean = photoData.value?.isNotEmpty() ?: false + + fun updateDescription(position: Int, description: String) { + photoData.value?.getOrNull(position)?.imageDescription = description + photoData.value = photoData.value + } + + fun resetUploadStatus() { + photoData.value = photoData.value?.map { it.copy(uploadId = null, progress = null) }?.toMutableList() + } + + fun setVideoEncodeAtPosition(position: Int, progress: Int?) { + photoData.value?.set(position, photoData.value!![position].copy(videoEncodeProgress = progress)) + photoData.value = photoData.value + } + + fun setUriAtPosition(uri: Uri, position: Int) { + photoData.value?.set(position, photoData.value!![position].copy(imageUri = uri)) + photoData.value = photoData.value + } + + fun setSizeAtPosition(imageSize: Long, position: Int) { + photoData.value?.set(position, photoData.value!![position].copy(size = imageSize)) + photoData.value = photoData.value + } + + fun removeAt(currentPosition: Int) { + photoData.value?.removeAt(currentPosition) + _uiState.update { + it.copy( + addPhotoButtonEnabled = true + ) + } + photoData.value = photoData.value + } + + /** + * Uploads the images that are in the [photoData] array. + * Keeps track of them in the [PhotoData.progress] (for the upload progress), and the + * [PhotoData.uploadId] (for the list of ids of the uploads). + */ + fun upload() { + _uiState.update { currentUiState -> + currentUiState.copy( + postCreationSendButtonEnabled = false, + addPhotoButtonEnabled = false, + editPhotoButtonEnabled = false, + removePhotoButtonEnabled = false, + uploadCompletedTextviewVisible = false, + uploadErrorVisible = false, + uploadProgressBarVisible = true + ) + } + + for (data: PhotoData in getPhotoData().value ?: emptyList()) { + val imageUri = data.imageUri + val imageInputStream = try { + getApplication().contentResolver.openInputStream(imageUri)!! + } catch (e: FileNotFoundException){ + _uiState.update { currentUiState -> + currentUiState.copy( + userMessage = getApplication().getString(R.string.file_not_found, + imageUri) + ) + } + return + } + + val imagePart = ProgressRequestBody(imageInputStream, data.size) + val requestBody = MultipartBody.Builder() + .setType(MultipartBody.FORM) + .addFormDataPart("file", System.currentTimeMillis().toString(), imagePart) + .build() + + val sub = imagePart.progressSubject + .subscribeOn(Schedulers.io()) + .subscribe { percentage -> + data.progress = percentage.toInt() + _uiState.update { currentUiState -> + currentUiState.copy( + uploadProgress = getPhotoData().value!!.sumOf { it.progress ?: 0 } / getPhotoData().value!!.size + ) + } + } + + var postSub: Disposable? = null + + val description = data.imageDescription?.let { MultipartBody.Part.createFormData("description", it) } + + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + val inter = api.mediaUpload(description, requestBody.parts[0]) + + postSub = inter + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + { attachment: Attachment -> + data.progress = 0 + data.uploadId = attachment.id!! + }, + { e: Throwable -> + _uiState.update { currentUiState -> + currentUiState.copy( + uploadErrorVisible = true, + uploadErrorExplanationText = if(e is HttpException){ + getApplication().getString(R.string.upload_error, e.code()) + } else "", + uploadErrorExplanationVisible = e is HttpException, + ) + } + e.printStackTrace() + postSub?.dispose() + sub.dispose() + }, + { + data.progress = 100 + if (getPhotoData().value!!.all { it.progress == 100 && it.uploadId != null }) { + _uiState.update { currentUiState -> + currentUiState.copy( + uploadProgressBarVisible = false, + uploadCompletedTextviewVisible = true + ) + } + post() + } + postSub?.dispose() + sub.dispose() + } + ) + } + } + + private fun post() { + val description = uiState.value.newPostDescriptionText + _uiState.update { currentUiState -> + currentUiState.copy( + postCreationSendButtonEnabled = false + ) + } + viewModelScope.launch { + try { + val api = apiHolder.api ?: apiHolder.setToCurrentUser() + + api.postStatus( + statusText = description, + media_ids = getPhotoData().value!!.mapNotNull { it.uploadId }.toList() + ) + Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_success), + Toast.LENGTH_SHORT).show() + val intent = Intent(getApplication(), MainActivity::class.java) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK + //TODO make the activity launch this instead (and surrounding toasts too)L + getApplication().startActivity(intent) + } catch (exception: IOException) { + Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_error), + Toast.LENGTH_SHORT).show() + Log.e(TAG, exception.toString()) + _uiState.update { currentUiState -> + currentUiState.copy( + postCreationSendButtonEnabled = true + ) + } + } catch (exception: HttpException) { + Toast.makeText(getApplication(), getApplication().getString(R.string.upload_post_failed), + Toast.LENGTH_SHORT).show() + Log.e(TAG, exception.response().toString() + exception.message().toString()) + _uiState.update { currentUiState -> + currentUiState.copy( + postCreationSendButtonEnabled = true + ) + } + } + } + } + + fun modifyAt(position: Int, data: Intent): Unit? { + val result: PhotoData = photoData.value?.get(position)?.run { + if (video) { + val muted: Boolean = data.getBooleanExtra(VideoEditActivity.MUTED, false) + val videoStart: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_START, -1f).let { + if(it == -1f) null else it + } + val modified: Boolean = data.getBooleanExtra(VideoEditActivity.MODIFIED, false) + val videoEnd: Float? = data.getFloatExtra(VideoEditActivity.VIDEO_END, -1f).let { + if(it == -1f) null else it + } + if(modified){ + videoEncodeProgress = 0 + sessionMap[position]?.let { FFmpegKit.cancel(it) } + _uiState.update { currentUiState -> + currentUiState.copy( + newEncodingJobPosition = position, + newEncodingJobMuted = muted, + newEncodingJobVideoStart = videoStart, + newEncodingJobVideoEnd = videoEnd + ) + } + } + } else { + imageUri = data.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri() + val (imageSize, imageVideo) = getSizeAndVideoValidate(imageUri, position) + size = imageSize + video = imageVideo + } + progress = null + uploadId = null + this + } ?: return null + result.let { + photoData.value?.set(position, it) + photoData.value = photoData.value + } + return Unit + } + + fun newPostDescriptionChanged(text: Editable?) { + _uiState.update { it.copy(newPostDescriptionText = text.toString()) } + } + + fun trackTempFile(file: File) { + tempFiles.add(file) + } + + fun cancelEncode(currentPosition: Int) { + sessionMap[currentPosition]?.let { FFmpegKit.cancel(it) } + } + + override fun onCleared() { + super.onCleared() + FFmpegKit.cancel() + tempFiles.forEach { + it.delete() + } + + } + + fun registerNewFFmpegSession(position: Int, sessionId: Long) { + sessionMap[position] = sessionId + } + + fun becameCarousel(became: Boolean) { + _uiState.update { currentUiState -> + currentUiState.copy( + isCarousel = became + ) + } + } + } -class PostCreationViewModelFactory(val bundle: ClipData? = null) : ViewModelProvider.Factory { - override fun create(modelClass: Class): T { - return modelClass.getConstructor(ClipData::class.java).newInstance(bundle) - } +class PostCreationViewModelFactory(val application: Application, val clipdata: ClipData, val instance: InstanceDatabaseEntity) : ViewModelProvider.Factory { + override fun create(modelClass: Class): T { + return modelClass.getConstructor(Application::class.java, ClipData::class.java, InstanceDatabaseEntity::class.java).newInstance(application, clipdata, instance) + } } \ No newline at end of file diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt index bb2473aa..58712df5 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/carousel/ImageCarousel.kt @@ -59,15 +59,7 @@ class ImageCarousel( initIndicator() } - - - private var btnPrevious: View? = null - private var btnNext: View? = null - - private var btnGrid: View? = null - private var btnCarousel: View? = null - - + private var isBuiltInIndicator = false private var data: MutableList? = null @@ -231,27 +223,24 @@ class ImageCarousel( set(value) { field = value - btnGrid = binding.switchToGridButton - btnCarousel = binding.switchToCarouselButton - - btnGrid?.setOnClickListener { + binding.switchToGridButton.setOnClickListener { layoutCarousel = false } - btnCarousel?.setOnClickListener { + binding.switchToCarouselButton.setOnClickListener { layoutCarousel = true } if(value){ if(layoutCarousel){ - btnGrid?.visibility = VISIBLE - btnCarousel?.visibility = GONE + binding.switchToGridButton.visibility = VISIBLE + binding.switchToCarouselButton.visibility = GONE } else { - btnGrid?.visibility = GONE - btnCarousel?.visibility = VISIBLE + binding.switchToGridButton.visibility = GONE + binding.switchToCarouselButton.visibility = VISIBLE } } else { - btnGrid?.visibility = GONE - btnCarousel?.visibility = GONE + binding.switchToGridButton.visibility = GONE + binding.switchToCarouselButton.visibility = GONE } } @@ -267,15 +256,15 @@ class ImageCarousel( if(value){ recyclerView.layoutManager = CarouselLinearLayoutManager(context, LinearLayoutManager.HORIZONTAL, false) - btnNext?.visibility = VISIBLE - btnPrevious?.visibility = VISIBLE + binding.btnNext.visibility = VISIBLE + binding.btnPrevious.visibility = VISIBLE binding.editMediaDescriptionLayout.visibility = if(editingMediaDescription) VISIBLE else INVISIBLE tvCaption.visibility = if(editingMediaDescription) INVISIBLE else VISIBLE } else { recyclerView.layoutManager = GridLayoutManager(context, 3) - btnNext?.visibility = GONE - btnPrevious?.visibility = GONE + binding.btnNext.visibility = GONE + binding.btnPrevious.visibility = GONE binding.editMediaDescriptionLayout.visibility = INVISIBLE tvCaption.visibility = INVISIBLE diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt index 11d0526a..22f92dc9 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/photoEdit/VideoEditActivity.kt @@ -15,8 +15,6 @@ import android.view.MenuItem import android.view.View import android.widget.FrameLayout import android.widget.ImageView -import android.widget.SeekBar -import android.widget.SeekBar.OnSeekBarChangeListener import androidx.core.net.toUri import androidx.core.os.HandlerCompat import androidx.media.AudioAttributesCompat @@ -33,10 +31,6 @@ import org.pixeldroid.app.postCreation.carousel.dpToPx import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.ffmpegSafeUri import java.io.File -import java.text.NumberFormat -import java.time.format.DateTimeFormatter -import java.util.* -import kotlin.collections.ArrayList class VideoEditActivity : BaseActivity() { diff --git a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt index 3a59eec0..0a646b08 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/di/ApplicationComponent.kt @@ -7,6 +7,7 @@ import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.db.AppDatabase import org.pixeldroid.app.utils.BaseFragment import dagger.Component +import org.pixeldroid.app.postCreation.PostCreationViewModel import org.pixeldroid.app.utils.notificationsWorker.NotificationsWorker import javax.inject.Singleton @@ -18,6 +19,7 @@ interface ApplicationComponent { fun inject(activity: BaseActivity?) fun inject(feedFragment: BaseFragment) fun inject(notificationsWorker: NotificationsWorker) + fun inject(postCreationViewModel: PostCreationViewModel) val context: Context? val application: Application? From 3c7f46d75a5f9a2f4dacee89ec95e218244e81b2 Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sun, 19 Jun 2022 14:28:00 +0200 Subject: [PATCH 4/5] Fix bug where all saved files were pngs --- .../app/postCreation/PostCreationActivity.kt | 42 ++++++++----------- .../java/org/pixeldroid/app/utils/Utils.kt | 7 ++++ 2 files changed, 25 insertions(+), 24 deletions(-) diff --git a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt index 63cd86af..2f54a5e0 100644 --- a/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt +++ b/app/src/main/java/org/pixeldroid/app/postCreation/PostCreationActivity.kt @@ -19,7 +19,6 @@ import androidx.activity.viewModels import androidx.core.net.toFile import androidx.core.net.toUri import androidx.core.os.HandlerCompat -import androidx.core.widget.addTextChangedListener import androidx.core.widget.doAfterTextChanged import androidx.lifecycle.Lifecycle import androidx.lifecycle.lifecycleScope @@ -27,29 +26,20 @@ import androidx.lifecycle.repeatOnLifecycle import androidx.recyclerview.widget.RecyclerView import com.arthenica.ffmpegkit.* import com.google.android.material.snackbar.Snackbar -import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers -import io.reactivex.rxjava3.disposables.Disposable -import io.reactivex.rxjava3.schedulers.Schedulers import kotlinx.coroutines.flow.collect import kotlinx.coroutines.launch -import okhttp3.MultipartBody -import org.pixeldroid.app.MainActivity import org.pixeldroid.app.R import org.pixeldroid.app.databinding.ActivityPostCreationBinding import org.pixeldroid.app.postCreation.camera.CameraActivity import org.pixeldroid.app.postCreation.carousel.CarouselItem -import org.pixeldroid.app.postCreation.carousel.ImageCarousel import org.pixeldroid.app.postCreation.photoEdit.PhotoEditActivity import org.pixeldroid.app.postCreation.photoEdit.VideoEditActivity import org.pixeldroid.app.utils.BaseActivity -import org.pixeldroid.app.utils.api.objects.Attachment import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity import org.pixeldroid.app.utils.ffmpegSafeUri -import retrofit2.HttpException +import org.pixeldroid.app.utils.fileExtension import java.io.File -import java.io.FileNotFoundException -import java.io.IOException import java.io.OutputStream import java.text.SimpleDateFormat import java.util.* @@ -214,13 +204,13 @@ class PostCreationActivity : BaseActivity() { } private fun savePicture(button: View, currentPosition: Int) { - val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US) - .format(System.currentTimeMillis()) + ".png" - val pair = getOutputFile(name) + val originalUri = model.getPhotoData().value!![currentPosition].imageUri + + val pair = getOutputFile(originalUri) val outputStream: OutputStream = pair.first val path: String = pair.second - contentResolver.openInputStream(model.getPhotoData().value!![currentPosition].imageUri)!!.use { input -> + contentResolver.openInputStream(originalUri)!!.use { input -> outputStream.use { output -> input.copyTo(output) } @@ -246,20 +236,28 @@ class PostCreationActivity : BaseActivity() { ).show() } - private fun getOutputFile(name: String): Pair { + private fun getOutputFile(uri: Uri): Pair { + val extension = uri.fileExtension(contentResolver) + + val name = SimpleDateFormat("yyyy-MM-dd-HH-mm-ss-SSS", Locale.US) + .format(System.currentTimeMillis()) + ".$extension" + val outputStream: OutputStream val path: String if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { val resolver: ContentResolver = contentResolver + val type = resolver.getType(uri) val contentValues = ContentValues() contentValues.put(MediaStore.MediaColumns.DISPLAY_NAME, name) - contentValues.put(MediaStore.MediaColumns.MIME_TYPE, "image/png") + contentValues.put(MediaStore.MediaColumns.MIME_TYPE, type) contentValues.put( MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_PICTURES ) - val imageUri: Uri = - resolver.insert(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, contentValues)!! + val imageUri: Uri = resolver.insert( + if (type?.startsWith("image") == true) MediaStore.Images.Media.EXTERNAL_CONTENT_URI + else MediaStore.Video.Media.EXTERNAL_CONTENT_URI, + contentValues)!! path = imageUri.toString() outputStream = resolver.openOutputStream(Objects.requireNonNull(imageUri))!! } else { @@ -327,11 +325,7 @@ class PostCreationActivity : BaseActivity() { val originalUri = model.getPhotoData().value!![position].imageUri // Having a meaningful suffix is necessary so that ffmpeg knows what to put in output - val suffix = if(originalUri.scheme == "content") { - contentResolver.getType(model.getPhotoData().value!![position].imageUri)?.takeLastWhile { it != '/' } - } else { - originalUri.toString().takeLastWhile { it != '/' } - } + val suffix = originalUri.fileExtension(contentResolver) val file = File.createTempFile("temp_video", ".$suffix") //val file = File.createTempFile("temp_video", ".webm") model.trackTempFile(file) diff --git a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt index b2ef5187..e0335d88 100644 --- a/app/src/main/java/org/pixeldroid/app/utils/Utils.kt +++ b/app/src/main/java/org/pixeldroid/app/utils/Utils.kt @@ -45,6 +45,13 @@ fun validDomain(domain: String?): Boolean { return true } +fun Uri.fileExtension(contentResolver: ContentResolver): String? { + return if (scheme == "content") { + contentResolver.getType(this)?.takeLastWhile { it != '/' } + } else { + toString().takeLastWhile { it != '/' } + } +} fun Context.displayDimensionsInPx(): Pair { val windowManager = getSystemService(Context.WINDOW_SERVICE) as WindowManager From 8201bb6bae131fd3ba502c5faa4c0d5eee633d8d Mon Sep 17 00:00:00 2001 From: Matthieu <24-artectrex@users.noreply.shinice.net> Date: Sun, 19 Jun 2022 16:29:59 +0200 Subject: [PATCH 5/5] Update licenses and switch to ffmpeg-kit-min --- app/build.gradle | 2 +- app/licenses.yml | 30 ++++++++++++++++++++++++------ app/src/main/assets/licenses.json | 2 +- 3 files changed, 26 insertions(+), 8 deletions(-) diff --git a/app/build.gradle b/app/build.gradle index e09ea30c..84977ceb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -154,7 +154,7 @@ dependencies { */ implementation 'com.davemorrissey.labs:subsampling-scale-image-view-androidx:3.10.0' - implementation 'com.arthenica:ffmpeg-kit-full:4.5.1-1.LTS' + implementation 'com.arthenica:ffmpeg-kit-min:4.5.1-1.LTS' implementation 'com.google.android.material:material:1.6.1' diff --git a/app/licenses.yml b/app/licenses.yml index 39aafb07..de647390 100644 --- a/app/licenses.yml +++ b/app/licenses.yml @@ -526,12 +526,6 @@ copyrightHolder: Google Inc. license: The Apache Software License, Version 2.0 licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt -- artifact: androidx.lifecycle:lifecycle-extensions:+ - name: lifecycle-extensions - copyrightHolder: Google Inc. - license: The Apache Software License, Version 2.0 - licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt - url: https://developer.android.com/topic/libraries/architecture/index.html - artifact: androidx.lifecycle:lifecycle-process:+ name: lifecycle-process copyrightHolder: Google Inc. @@ -955,3 +949,27 @@ license: The Apache Software License, Version 2.0 licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt url: https://developer.android.com/jetpack/androidx/releases/media2 +- artifact: com.davemorrissey.labs:subsampling-scale-image-view-androidx:+ + name: subsampling-scale-image-view-androidx + copyrightHolder: David Morrissey and contributors + license: The Apache Software License, Version 2.0 + licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt + url: https://github.com/davemorrissey/subsampling-scale-image-view +- artifact: com.arthenica:ffmpeg-kit-min:+ + name: ffmpeg-kit-min + copyrightHolder: Taner Şener + license: GNU Lesser General Public License, Version 3 + licenseUrl: https://www.gnu.org/licenses/lgpl-3.0.txt + url: https://github.com/tanersener/ffmpeg-kit +- artifact: com.arthenica:smart-exception-java:+ + name: smart-exception-java + copyrightHolder: Taner Şener + license: The 3-Clause BSD License + licenseUrl: https://opensource.org/licenses/BSD-3-Clause + url: https://github.com/tanersener/smart-exception +- artifact: com.arthenica:smart-exception-common:+ + name: smart-exception-common + copyrightHolder: Taner Şener + license: The 3-Clause BSD License + licenseUrl: https://opensource.org/licenses/BSD-3-Clause + url: https://github.com/tanersener/smart-exception diff --git a/app/src/main/assets/licenses.json b/app/src/main/assets/licenses.json index 3d00366e..93b50f51 100644 --- a/app/src/main/assets/licenses.json +++ b/app/src/main/assets/licenses.json @@ -1 +1 @@ -{"libraries":[{"artifactId":{"name":"materialdrawer-nav","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/MaterialDrawer","libraryName":"materialdrawer-nav"},{"artifactId":{"name":"materialdrawer-iconics","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/MaterialDrawer","libraryName":"materialdrawer-iconics"},{"artifactId":{"name":"materialdrawer","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/MaterialDrawer","libraryName":"materialdrawer"},{"artifactId":{"name":"startup-runtime","group":"androidx.startup","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/startup#1.0.0","libraryName":"startup-runtime"},{"artifactId":{"name":"iconics-views","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"iconics-views"},{"artifactId":{"name":"iconics-core","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"iconics-core"},{"artifactId":{"name":"iconics-typeface-api","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"iconics-typeface-api"},{"artifactId":{"name":"kotlin-stdlib-jdk8","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib-jdk8"},{"artifactId":{"name":"kotlin-stdlib-jdk7","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib-jdk7"},{"artifactId":{"name":"preference","group":"androidx.preference","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"preference"},{"artifactId":{"name":"constraintlayout","group":"androidx.constraintlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://tools.android.com","libraryName":"constraintlayout"},{"artifactId":{"name":"camera-view","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-alpha15","libraryName":"camera-view"},{"artifactId":{"name":"navigation-ui-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-ui-ktx"},{"artifactId":{"name":"navigation-ui","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-ui"},{"artifactId":{"name":"dexter","group":"com.karumi","version":"+"},"copyrightHolder":"Karumi and contributors","copyrightStatement":"Copyright © Karumi and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/karumi/Dexter","libraryName":"dexter"},{"artifactId":{"name":"material","group":"com.google.android.material","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/material-components/material-components-android","libraryName":"material"},{"artifactId":{"name":"dagger-android-support","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger-android-support"},{"artifactId":{"name":"imagefilters","group":"info.androidhive","version":"+"},"copyrightHolder":"Zomato and ravi8x","copyrightStatement":"Copyright © Zomato and ravi8x. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"imagefilters"},{"artifactId":{"name":"ucrop","group":"com.github.yalantis","version":"+"},"copyrightHolder":"Yalantis","copyrightStatement":"Copyright © Yalantis. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0","normalizedLicense":"apache2","url":"https://github.com/Yalantis/uCrop","libraryName":"ucrop"},{"artifactId":{"name":"tracedroid","group":"com.github.ligi","version":"+"},"copyrightHolder":"ligi","copyrightStatement":"Copyright © ligi. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","url":"https://github.com/ligi/tracedroid","libraryName":"tracedroid"},{"artifactId":{"name":"supportemail","group":"com.github.ligi.tracedroid","version":"+"},"copyrightHolder":"ligi","copyrightStatement":"Copyright © ligi. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","url":"https://github.com/ligi/tracedroid","libraryName":"supportemail"},{"artifactId":{"name":"lib","group":"com.github.ligi.tracedroid","version":"+"},"copyrightHolder":"ligi","copyrightStatement":"Copyright © ligi. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","url":"https://github.com/ligi/tracedroid","libraryName":"lib"},{"artifactId":{"name":"timber","group":"com.jakewharton.timber","version":"+"},"copyrightHolder":"Jake Wharton and contributors","copyrightStatement":"Copyright © Jake Wharton and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/JakeWharton/timber","libraryName":"timber"},{"artifactId":{"name":"fastadapter-extensions-expandable","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/FastAdapter","libraryName":"fastadapter-extensions-expandable"},{"artifactId":{"name":"fastadapter","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/FastAdapter","libraryName":"fastadapter"},{"artifactId":{"name":"appcompat","group":"androidx.appcompat","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"appcompat"},{"artifactId":{"name":"navigation-fragment-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-fragment-ktx"},{"artifactId":{"name":"fragment-ktx","group":"androidx.fragment","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"fragment-ktx"},{"artifactId":{"name":"navigation-runtime-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-runtime-ktx"},{"artifactId":{"name":"navigation-common-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-common-ktx"},{"artifactId":{"name":"activity-ktx","group":"androidx.activity","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"activity-ktx"},{"artifactId":{"name":"core-ktx","group":"androidx.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"core-ktx"},{"artifactId":{"name":"navigation-fragment","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-fragment"},{"artifactId":{"name":"browser","group":"androidx.browser","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"browser"},{"artifactId":{"name":"paging-runtime-ktx","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"paging-runtime-ktx"},{"artifactId":{"name":"paging-runtime","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"paging-runtime"},{"artifactId":{"name":"viewpager2","group":"androidx.viewpager2","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"viewpager2"},{"artifactId":{"name":"recyclerview","group":"androidx.recyclerview","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"recyclerview"},{"artifactId":{"name":"legacy-support-v4","group":"androidx.legacy","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"legacy-support-v4"},{"artifactId":{"name":"legacy-support-core-ui","group":"androidx.legacy","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"legacy-support-core-ui"},{"artifactId":{"name":"swiperefreshlayout","group":"androidx.swiperefreshlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"swiperefreshlayout"},{"artifactId":{"name":"lifecycle-livedata-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-livedata-ktx"},{"artifactId":{"name":"okhttp-integration","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"okhttp-integration"},{"artifactId":{"name":"glide","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"glide"},{"artifactId":{"name":"fragment","group":"androidx.fragment","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"fragment"},{"artifactId":{"name":"navigation-runtime","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-runtime"},{"artifactId":{"name":"activity","group":"androidx.activity","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"activity"},{"artifactId":{"name":"lifecycle-viewmodel-savedstate","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-viewmodel-savedstate"},{"artifactId":{"name":"lifecycle-runtime-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-runtime-ktx"},{"artifactId":{"name":"camera-camera2","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-beta08","libraryName":"camera-camera2"},{"artifactId":{"name":"camera-lifecycle","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-beta08","libraryName":"camera-lifecycle"},{"artifactId":{"name":"camera-core","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-beta08","libraryName":"camera-core"},{"artifactId":{"name":"dagger-android","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger-android"},{"artifactId":{"name":"cursoradapter","group":"androidx.cursoradapter","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"cursoradapter"},{"artifactId":{"name":"appcompat-resources","group":"androidx.appcompat","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"appcompat-resources"},{"artifactId":{"name":"drawerlayout","group":"androidx.drawerlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"drawerlayout"},{"artifactId":{"name":"coordinatorlayout","group":"androidx.coordinatorlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"coordinatorlayout"},{"artifactId":{"name":"viewpager","group":"androidx.viewpager","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"viewpager"},{"artifactId":{"name":"slidingpanelayout","group":"androidx.slidingpanelayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"slidingpanelayout"},{"artifactId":{"name":"customview","group":"androidx.customview","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"customview"},{"artifactId":{"name":"transition","group":"androidx.transition","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"transition"},{"artifactId":{"name":"vectordrawable-animated","group":"androidx.vectordrawable","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"vectordrawable-animated"},{"artifactId":{"name":"vectordrawable","group":"androidx.vectordrawable","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"vectordrawable"},{"artifactId":{"name":"media","group":"androidx.media","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"media"},{"artifactId":{"name":"legacy-support-core-utils","group":"androidx.legacy","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"legacy-support-core-utils"},{"artifactId":{"name":"loader","group":"androidx.loader","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"loader"},{"artifactId":{"name":"navigation-common","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-common"},{"artifactId":{"name":"asynclayoutinflater","group":"androidx.asynclayoutinflater","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"asynclayoutinflater"},{"artifactId":{"name":"core","group":"androidx.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"core"},{"artifactId":{"name":"versionedparcelable","group":"androidx.versionedparcelable","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"versionedparcelable"},{"artifactId":{"name":"collection-ktx","group":"androidx.collection","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"collection-ktx"},{"artifactId":{"name":"collection","group":"androidx.collection","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"collection"},{"artifactId":{"name":"concurrent-futures","group":"androidx.concurrent","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"concurrent-futures"},{"artifactId":{"name":"interpolator","group":"androidx.interpolator","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"interpolator"},{"artifactId":{"name":"savedstate","group":"androidx.savedstate","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"savedstate"},{"artifactId":{"name":"lifecycle-viewmodel-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-viewmodel-ktx"},{"artifactId":{"name":"lifecycle-viewmodel","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-viewmodel"},{"artifactId":{"name":"lifecycle-runtime","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-runtime"},{"artifactId":{"name":"room-ktx","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"room-ktx"},{"artifactId":{"name":"room-runtime","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"room-runtime"},{"artifactId":{"name":"room-common","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"room-common"},{"artifactId":{"name":"sqlite-framework","group":"androidx.sqlite","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"sqlite-framework"},{"artifactId":{"name":"sqlite","group":"androidx.sqlite","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"sqlite"},{"artifactId":{"name":"cardview","group":"androidx.cardview","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"cardview"},{"artifactId":{"name":"exifinterface","group":"androidx.exifinterface","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"exifinterface"},{"artifactId":{"name":"gifdecoder","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"gifdecoder"},{"artifactId":{"name":"paging-common-ktx","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"paging-common-ktx"},{"artifactId":{"name":"constraintlayout-core","group":"androidx.constraintlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://tools.android.com","libraryName":"constraintlayout-core"},{"artifactId":{"name":"databinding-ktx","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"databinding-ktx"},{"artifactId":{"name":"lifecycle-extensions","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-extensions"},{"artifactId":{"name":"lifecycle-process","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-process"},{"artifactId":{"name":"lifecycle-service","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-service"},{"artifactId":{"name":"work-runtime-ktx","group":"androidx.work","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/work#2.5.0","libraryName":"work-runtime-ktx"},{"artifactId":{"name":"work-runtime","group":"androidx.work","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/work#2.5.0","libraryName":"work-runtime"},{"artifactId":{"name":"paging-common","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"paging-common"},{"artifactId":{"name":"lifecycle-livedata","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-livedata"},{"artifactId":{"name":"lifecycle-livedata-core-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-livedata-core-ktx"},{"artifactId":{"name":"lifecycle-livedata-core","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-livedata-core"},{"artifactId":{"name":"core-runtime","group":"androidx.arch.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"core-runtime"},{"artifactId":{"name":"core-common","group":"androidx.arch.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"core-common"},{"artifactId":{"name":"lifecycle-common","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-common"},{"artifactId":{"name":"documentfile","group":"androidx.documentfile","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"documentfile"},{"artifactId":{"name":"localbroadcastmanager","group":"androidx.localbroadcastmanager","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"localbroadcastmanager"},{"artifactId":{"name":"print","group":"androidx.print","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"print"},{"artifactId":{"name":"annotation","group":"androidx.annotation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"annotation"},{"artifactId":{"name":"converter-gson","group":"com.squareup.retrofit2","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/retrofit","libraryName":"converter-gson"},{"artifactId":{"name":"adapter-rxjava3","group":"com.squareup.retrofit2","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/retrofit","libraryName":"adapter-rxjava3"},{"artifactId":{"name":"retrofit","group":"com.squareup.retrofit2","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/retrofit","libraryName":"retrofit"},{"artifactId":{"name":"okhttp","group":"com.squareup.okhttp3","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://square.github.io/okhttp/","libraryName":"okhttp"},{"artifactId":{"name":"rxandroid","group":"io.reactivex.rxjava3","version":"+"},"copyrightHolder":"Netflix, Inc","copyrightStatement":"Copyright © Netflix, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/ReactiveX/RxAndroid","libraryName":"rxandroid"},{"artifactId":{"name":"rxjava","group":"io.reactivex.rxjava3","version":"+"},"copyrightHolder":"Netflix, Inc.","copyrightStatement":"Copyright © Netflix, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/ReactiveX/RxJava","libraryName":"rxjava"},{"artifactId":{"name":"sparkbutton","group":"com.github.connyduck","version":"+"},"copyrightHolder":"varunest and ConnyDuck","copyrightStatement":"Copyright © varunest and ConnyDuck. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/connyduck/sparkbutton","libraryName":"sparkbutton"},{"artifactId":{"name":"recyclerview-integration","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"recyclerview-integration"},{"artifactId":{"name":"google-material-typeface","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"google-material-typeface"},{"artifactId":{"name":"kotlinx-coroutines-android","group":"org.jetbrains.kotlinx","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/Kotlin/kotlinx.coroutines","libraryName":"kotlinx-coroutines-android"},{"artifactId":{"name":"viewbinding","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"viewbinding"},{"artifactId":{"name":"kotlinx-coroutines-core-jvm","group":"org.jetbrains.kotlinx","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/Kotlin/kotlinx.coroutines","libraryName":"kotlinx-coroutines-core-jvm"},{"artifactId":{"name":"okhttp","group":"com.squareup.okhttp","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"okhttp"},{"artifactId":{"name":"okio","group":"com.squareup.okio","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/okio/","libraryName":"okio"},{"artifactId":{"name":"kotlin-stdlib","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib"},{"artifactId":{"name":"listenablefuture","group":"com.google.guava","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"listenablefuture"},{"artifactId":{"name":"annotation-experimental","group":"androidx.annotation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"annotation-experimental"},{"artifactId":{"name":"auto-value-annotations","group":"com.google.auto.value","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"auto-value-annotations"},{"artifactId":{"name":"dagger","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger"},{"artifactId":{"name":"dagger-lint-aar","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger-lint-aar"},{"artifactId":{"name":"javax.inject","group":"javax.inject","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://code.google.com/p/atinject/","libraryName":"javax.inject"},{"artifactId":{"name":"gson","group":"com.google.code.gson","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"gson"},{"artifactId":{"name":"disklrucache","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"disklrucache"},{"artifactId":{"name":"annotations","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"annotations"},{"artifactId":{"name":"kotlin-stdlib-common","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib-common"},{"artifactId":{"name":"annotations","group":"org.jetbrains","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://www.jetbrains.org","libraryName":"annotations"},{"artifactId":{"name":"gridlayout","group":"androidx.gridlayout","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"gridlayout"},{"artifactId":{"name":"preference-ktx","group":"androidx.preference","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"preference-ktx"},{"artifactId":{"name":"databinding-adapters","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"databinding-adapters"},{"artifactId":{"name":"databinding-runtime","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"databinding-runtime"},{"artifactId":{"name":"databinding-common","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/studio","libraryName":"databinding-common"},{"artifactId":{"name":"lifecycle-common-java8","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-common-java8"},{"artifactId":{"name":"circleindicator","group":"me.relex","version":"+"},"copyrightHolder":"relex and contributors","copyrightStatement":"Copyright © relex and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/ongakuer/CircleIndicator","libraryName":"circleindicator"},{"artifactId":{"name":"dynamicanimation","group":"androidx.dynamicanimation","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"dynamicanimation"},{"artifactId":{"name":"savedstate-ktx","group":"androidx.savedstate","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0","libraryName":"savedstate-ktx"},{"artifactId":{"name":"tracing","group":"androidx.tracing","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0","libraryName":"tracing"},{"artifactId":{"name":"emoji2-views-helper","group":"androidx.emoji2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0","libraryName":"emoji2-views-helper"},{"artifactId":{"name":"emoji2","group":"androidx.emoji2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0","libraryName":"emoji2"},{"artifactId":{"name":"resourceinspection-annotation","group":"androidx.resourceinspection","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/resourceinspection#1.0.0","libraryName":"resourceinspection-annotation"},{"artifactId":{"name":"room-paging","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/room#2.4.1","libraryName":"room-paging"},{"artifactId":{"name":"window","group":"androidx.window","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/window#1.0.0","libraryName":"window"},{"artifactId":{"name":"animated-vector-drawable","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"animated-vector-drawable"},{"artifactId":{"name":"appcompat-v7","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"appcompat-v7"},{"artifactId":{"name":"support-annotations","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-annotations"},{"artifactId":{"name":"support-compat","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-compat"},{"artifactId":{"name":"support-core-ui","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-core-ui"},{"artifactId":{"name":"support-core-utils","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-core-utils"},{"artifactId":{"name":"support-fragment","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-fragment"},{"artifactId":{"name":"support-media-compat","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-media-compat"},{"artifactId":{"name":"support-v4","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-v4"},{"artifactId":{"name":"support-vector-drawable","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-vector-drawable"}]} \ No newline at end of file +{"libraries":[{"artifactId":{"name":"materialdrawer-nav","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/MaterialDrawer","libraryName":"materialdrawer-nav"},{"artifactId":{"name":"materialdrawer-iconics","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/MaterialDrawer","libraryName":"materialdrawer-iconics"},{"artifactId":{"name":"materialdrawer","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/MaterialDrawer","libraryName":"materialdrawer"},{"artifactId":{"name":"startup-runtime","group":"androidx.startup","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/startup#1.0.0","libraryName":"startup-runtime"},{"artifactId":{"name":"iconics-views","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"iconics-views"},{"artifactId":{"name":"iconics-core","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"iconics-core"},{"artifactId":{"name":"iconics-typeface-api","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"iconics-typeface-api"},{"artifactId":{"name":"kotlin-stdlib-jdk8","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib-jdk8"},{"artifactId":{"name":"kotlin-stdlib-jdk7","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib-jdk7"},{"artifactId":{"name":"preference","group":"androidx.preference","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"preference"},{"artifactId":{"name":"constraintlayout","group":"androidx.constraintlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://tools.android.com","libraryName":"constraintlayout"},{"artifactId":{"name":"camera-view","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-alpha15","libraryName":"camera-view"},{"artifactId":{"name":"navigation-ui-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-ui-ktx"},{"artifactId":{"name":"navigation-ui","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-ui"},{"artifactId":{"name":"dexter","group":"com.karumi","version":"+"},"copyrightHolder":"Karumi and contributors","copyrightStatement":"Copyright © Karumi and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/karumi/Dexter","libraryName":"dexter"},{"artifactId":{"name":"material","group":"com.google.android.material","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/material-components/material-components-android","libraryName":"material"},{"artifactId":{"name":"dagger-android-support","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger-android-support"},{"artifactId":{"name":"imagefilters","group":"info.androidhive","version":"+"},"copyrightHolder":"Zomato and ravi8x","copyrightStatement":"Copyright © Zomato and ravi8x. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"imagefilters"},{"artifactId":{"name":"ucrop","group":"com.github.yalantis","version":"+"},"copyrightHolder":"Yalantis","copyrightStatement":"Copyright © Yalantis. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0","normalizedLicense":"apache2","url":"https://github.com/Yalantis/uCrop","libraryName":"ucrop"},{"artifactId":{"name":"tracedroid","group":"com.github.ligi","version":"+"},"copyrightHolder":"ligi","copyrightStatement":"Copyright © ligi. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","url":"https://github.com/ligi/tracedroid","libraryName":"tracedroid"},{"artifactId":{"name":"supportemail","group":"com.github.ligi.tracedroid","version":"+"},"copyrightHolder":"ligi","copyrightStatement":"Copyright © ligi. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","url":"https://github.com/ligi/tracedroid","libraryName":"supportemail"},{"artifactId":{"name":"lib","group":"com.github.ligi.tracedroid","version":"+"},"copyrightHolder":"ligi","copyrightStatement":"Copyright © ligi. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","url":"https://github.com/ligi/tracedroid","libraryName":"lib"},{"artifactId":{"name":"timber","group":"com.jakewharton.timber","version":"+"},"copyrightHolder":"Jake Wharton and contributors","copyrightStatement":"Copyright © Jake Wharton and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/JakeWharton/timber","libraryName":"timber"},{"artifactId":{"name":"fastadapter-extensions-expandable","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/FastAdapter","libraryName":"fastadapter-extensions-expandable"},{"artifactId":{"name":"fastadapter","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/FastAdapter","libraryName":"fastadapter"},{"artifactId":{"name":"appcompat","group":"androidx.appcompat","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"appcompat"},{"artifactId":{"name":"navigation-fragment-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-fragment-ktx"},{"artifactId":{"name":"fragment-ktx","group":"androidx.fragment","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"fragment-ktx"},{"artifactId":{"name":"navigation-runtime-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-runtime-ktx"},{"artifactId":{"name":"navigation-common-ktx","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-common-ktx"},{"artifactId":{"name":"activity-ktx","group":"androidx.activity","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"activity-ktx"},{"artifactId":{"name":"core-ktx","group":"androidx.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"core-ktx"},{"artifactId":{"name":"navigation-fragment","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-fragment"},{"artifactId":{"name":"browser","group":"androidx.browser","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"browser"},{"artifactId":{"name":"paging-runtime-ktx","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"paging-runtime-ktx"},{"artifactId":{"name":"paging-runtime","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"paging-runtime"},{"artifactId":{"name":"viewpager2","group":"androidx.viewpager2","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"viewpager2"},{"artifactId":{"name":"recyclerview","group":"androidx.recyclerview","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"recyclerview"},{"artifactId":{"name":"legacy-support-v4","group":"androidx.legacy","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"legacy-support-v4"},{"artifactId":{"name":"legacy-support-core-ui","group":"androidx.legacy","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"legacy-support-core-ui"},{"artifactId":{"name":"swiperefreshlayout","group":"androidx.swiperefreshlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"swiperefreshlayout"},{"artifactId":{"name":"lifecycle-livedata-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-livedata-ktx"},{"artifactId":{"name":"okhttp-integration","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"okhttp-integration"},{"artifactId":{"name":"glide","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"glide"},{"artifactId":{"name":"fragment","group":"androidx.fragment","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"fragment"},{"artifactId":{"name":"navigation-runtime","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-runtime"},{"artifactId":{"name":"activity","group":"androidx.activity","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"activity"},{"artifactId":{"name":"lifecycle-viewmodel-savedstate","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-viewmodel-savedstate"},{"artifactId":{"name":"lifecycle-runtime-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-runtime-ktx"},{"artifactId":{"name":"camera-camera2","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-beta08","libraryName":"camera-camera2"},{"artifactId":{"name":"camera-lifecycle","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-beta08","libraryName":"camera-lifecycle"},{"artifactId":{"name":"camera-core","group":"androidx.camera","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/camera#1.0.0-beta08","libraryName":"camera-core"},{"artifactId":{"name":"dagger-android","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger-android"},{"artifactId":{"name":"cursoradapter","group":"androidx.cursoradapter","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"cursoradapter"},{"artifactId":{"name":"appcompat-resources","group":"androidx.appcompat","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"appcompat-resources"},{"artifactId":{"name":"drawerlayout","group":"androidx.drawerlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"drawerlayout"},{"artifactId":{"name":"coordinatorlayout","group":"androidx.coordinatorlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"coordinatorlayout"},{"artifactId":{"name":"viewpager","group":"androidx.viewpager","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"viewpager"},{"artifactId":{"name":"slidingpanelayout","group":"androidx.slidingpanelayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"slidingpanelayout"},{"artifactId":{"name":"customview","group":"androidx.customview","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"customview"},{"artifactId":{"name":"transition","group":"androidx.transition","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"transition"},{"artifactId":{"name":"vectordrawable-animated","group":"androidx.vectordrawable","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"vectordrawable-animated"},{"artifactId":{"name":"vectordrawable","group":"androidx.vectordrawable","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"vectordrawable"},{"artifactId":{"name":"media","group":"androidx.media","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"media"},{"artifactId":{"name":"legacy-support-core-utils","group":"androidx.legacy","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"legacy-support-core-utils"},{"artifactId":{"name":"loader","group":"androidx.loader","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"loader"},{"artifactId":{"name":"navigation-common","group":"androidx.navigation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"navigation-common"},{"artifactId":{"name":"asynclayoutinflater","group":"androidx.asynclayoutinflater","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"asynclayoutinflater"},{"artifactId":{"name":"core","group":"androidx.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"core"},{"artifactId":{"name":"versionedparcelable","group":"androidx.versionedparcelable","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"versionedparcelable"},{"artifactId":{"name":"collection-ktx","group":"androidx.collection","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"collection-ktx"},{"artifactId":{"name":"collection","group":"androidx.collection","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"collection"},{"artifactId":{"name":"concurrent-futures","group":"androidx.concurrent","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"concurrent-futures"},{"artifactId":{"name":"interpolator","group":"androidx.interpolator","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"interpolator"},{"artifactId":{"name":"savedstate","group":"androidx.savedstate","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"savedstate"},{"artifactId":{"name":"lifecycle-viewmodel-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-viewmodel-ktx"},{"artifactId":{"name":"lifecycle-viewmodel","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-viewmodel"},{"artifactId":{"name":"lifecycle-runtime","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-runtime"},{"artifactId":{"name":"room-ktx","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"room-ktx"},{"artifactId":{"name":"room-runtime","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"room-runtime"},{"artifactId":{"name":"room-common","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"room-common"},{"artifactId":{"name":"sqlite-framework","group":"androidx.sqlite","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"sqlite-framework"},{"artifactId":{"name":"sqlite","group":"androidx.sqlite","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"sqlite"},{"artifactId":{"name":"cardview","group":"androidx.cardview","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"cardview"},{"artifactId":{"name":"exifinterface","group":"androidx.exifinterface","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"exifinterface"},{"artifactId":{"name":"gifdecoder","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"gifdecoder"},{"artifactId":{"name":"paging-common-ktx","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"paging-common-ktx"},{"artifactId":{"name":"constraintlayout-core","group":"androidx.constraintlayout","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://tools.android.com","libraryName":"constraintlayout-core"},{"artifactId":{"name":"databinding-ktx","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"databinding-ktx"},{"artifactId":{"name":"lifecycle-process","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-process"},{"artifactId":{"name":"lifecycle-service","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-service"},{"artifactId":{"name":"work-runtime-ktx","group":"androidx.work","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/work#2.5.0","libraryName":"work-runtime-ktx"},{"artifactId":{"name":"work-runtime","group":"androidx.work","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/work#2.5.0","libraryName":"work-runtime"},{"artifactId":{"name":"paging-common","group":"androidx.paging","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"paging-common"},{"artifactId":{"name":"lifecycle-livedata","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-livedata"},{"artifactId":{"name":"lifecycle-livedata-core-ktx","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"lifecycle-livedata-core-ktx"},{"artifactId":{"name":"lifecycle-livedata-core","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-livedata-core"},{"artifactId":{"name":"core-runtime","group":"androidx.arch.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"core-runtime"},{"artifactId":{"name":"core-common","group":"androidx.arch.core","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"core-common"},{"artifactId":{"name":"lifecycle-common","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-common"},{"artifactId":{"name":"documentfile","group":"androidx.documentfile","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"documentfile"},{"artifactId":{"name":"localbroadcastmanager","group":"androidx.localbroadcastmanager","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"localbroadcastmanager"},{"artifactId":{"name":"print","group":"androidx.print","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"print"},{"artifactId":{"name":"annotation","group":"androidx.annotation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"annotation"},{"artifactId":{"name":"converter-gson","group":"com.squareup.retrofit2","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/retrofit","libraryName":"converter-gson"},{"artifactId":{"name":"adapter-rxjava3","group":"com.squareup.retrofit2","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/retrofit","libraryName":"adapter-rxjava3"},{"artifactId":{"name":"retrofit","group":"com.squareup.retrofit2","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/retrofit","libraryName":"retrofit"},{"artifactId":{"name":"okhttp","group":"com.squareup.okhttp3","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://square.github.io/okhttp/","libraryName":"okhttp"},{"artifactId":{"name":"rxandroid","group":"io.reactivex.rxjava3","version":"+"},"copyrightHolder":"Netflix, Inc","copyrightStatement":"Copyright © Netflix, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/ReactiveX/RxAndroid","libraryName":"rxandroid"},{"artifactId":{"name":"rxjava","group":"io.reactivex.rxjava3","version":"+"},"copyrightHolder":"Netflix, Inc.","copyrightStatement":"Copyright © Netflix, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/ReactiveX/RxJava","libraryName":"rxjava"},{"artifactId":{"name":"sparkbutton","group":"com.github.connyduck","version":"+"},"copyrightHolder":"varunest and ConnyDuck","copyrightStatement":"Copyright © varunest and ConnyDuck. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/connyduck/sparkbutton","libraryName":"sparkbutton"},{"artifactId":{"name":"recyclerview-integration","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"recyclerview-integration"},{"artifactId":{"name":"google-material-typeface","group":"com.mikepenz","version":"+"},"copyrightHolder":"Mike Penz and contributors","copyrightStatement":"Copyright © Mike Penz and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/mikepenz/Android-Iconics","libraryName":"google-material-typeface"},{"artifactId":{"name":"kotlinx-coroutines-android","group":"org.jetbrains.kotlinx","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/Kotlin/kotlinx.coroutines","libraryName":"kotlinx-coroutines-android"},{"artifactId":{"name":"viewbinding","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"viewbinding"},{"artifactId":{"name":"kotlinx-coroutines-core-jvm","group":"org.jetbrains.kotlinx","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/Kotlin/kotlinx.coroutines","libraryName":"kotlinx-coroutines-core-jvm"},{"artifactId":{"name":"okhttp","group":"com.squareup.okhttp","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"okhttp"},{"artifactId":{"name":"okio","group":"com.squareup.okio","version":"+"},"copyrightHolder":"Square, Inc.","copyrightStatement":"Copyright © Square, Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/square/okio/","libraryName":"okio"},{"artifactId":{"name":"kotlin-stdlib","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib"},{"artifactId":{"name":"listenablefuture","group":"com.google.guava","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"listenablefuture"},{"artifactId":{"name":"annotation-experimental","group":"androidx.annotation","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"annotation-experimental"},{"artifactId":{"name":"auto-value-annotations","group":"com.google.auto.value","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"auto-value-annotations"},{"artifactId":{"name":"dagger","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger"},{"artifactId":{"name":"dagger-lint-aar","group":"com.google.dagger","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Apache 2.0","licenseUrl":"https://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/google/dagger","libraryName":"dagger-lint-aar"},{"artifactId":{"name":"javax.inject","group":"javax.inject","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://code.google.com/p/atinject/","libraryName":"javax.inject"},{"artifactId":{"name":"gson","group":"com.google.code.gson","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","normalizedLicense":"apache2","libraryName":"gson"},{"artifactId":{"name":"disklrucache","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"disklrucache"},{"artifactId":{"name":"annotations","group":"com.github.bumptech.glide","version":"+"},"copyrightHolder":"Google Inc.","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"Simplified BSD License","licenseUrl":"http://www.opensource.org/licenses/bsd-license","normalizedLicense":"bsd_2_clauses","url":"https://github.com/bumptech/glide","libraryName":"annotations"},{"artifactId":{"name":"kotlin-stdlib-common","group":"org.jetbrains.kotlin","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://kotlinlang.org/","libraryName":"kotlin-stdlib-common"},{"artifactId":{"name":"annotations","group":"org.jetbrains","version":"+"},"copyrightHolder":"JetBrains s.r.o. and contributors","copyrightStatement":"Copyright © JetBrains s.r.o. and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://www.jetbrains.org","libraryName":"annotations"},{"artifactId":{"name":"gridlayout","group":"androidx.gridlayout","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"gridlayout"},{"artifactId":{"name":"preference-ktx","group":"androidx.preference","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx","libraryName":"preference-ktx"},{"artifactId":{"name":"databinding-adapters","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"databinding-adapters"},{"artifactId":{"name":"databinding-runtime","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","libraryName":"databinding-runtime"},{"artifactId":{"name":"databinding-common","group":"androidx.databinding","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/studio","libraryName":"databinding-common"},{"artifactId":{"name":"lifecycle-common-java8","group":"androidx.lifecycle","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/topic/libraries/architecture/index.html","libraryName":"lifecycle-common-java8"},{"artifactId":{"name":"circleindicator","group":"me.relex","version":"+"},"copyrightHolder":"relex and contributors","copyrightStatement":"Copyright © relex and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/ongakuer/CircleIndicator","libraryName":"circleindicator"},{"artifactId":{"name":"dynamicanimation","group":"androidx.dynamicanimation","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"dynamicanimation"},{"artifactId":{"name":"savedstate-ktx","group":"androidx.savedstate","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/savedstate#1.1.0","libraryName":"savedstate-ktx"},{"artifactId":{"name":"tracing","group":"androidx.tracing","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/tracing#1.0.0","libraryName":"tracing"},{"artifactId":{"name":"emoji2-views-helper","group":"androidx.emoji2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0","libraryName":"emoji2-views-helper"},{"artifactId":{"name":"emoji2","group":"androidx.emoji2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/emoji2#1.0.0","libraryName":"emoji2"},{"artifactId":{"name":"resourceinspection-annotation","group":"androidx.resourceinspection","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/resourceinspection#1.0.0","libraryName":"resourceinspection-annotation"},{"artifactId":{"name":"room-paging","group":"androidx.room","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/room#2.4.1","libraryName":"room-paging"},{"artifactId":{"name":"window","group":"androidx.window","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/window#1.0.0","libraryName":"window"},{"artifactId":{"name":"animated-vector-drawable","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"animated-vector-drawable"},{"artifactId":{"name":"appcompat-v7","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"appcompat-v7"},{"artifactId":{"name":"support-annotations","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-annotations"},{"artifactId":{"name":"support-compat","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-compat"},{"artifactId":{"name":"support-core-ui","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-core-ui"},{"artifactId":{"name":"support-core-utils","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-core-utils"},{"artifactId":{"name":"support-fragment","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-fragment"},{"artifactId":{"name":"support-media-compat","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-media-compat"},{"artifactId":{"name":"support-v4","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-v4"},{"artifactId":{"name":"support-vector-drawable","group":"com.android.support","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"support-vector-drawable"},{"artifactId":{"name":"media2-widget","group":"androidx.media2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/media2","libraryName":"media2-widget"},{"artifactId":{"name":"palette","group":"androidx.palette","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"http://developer.android.com/tools/extras/support-library.html","libraryName":"palette"},{"artifactId":{"name":"media2-player","group":"androidx.media2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/media2","libraryName":"media2-player"},{"artifactId":{"name":"media2-session","group":"androidx.media2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/media2","libraryName":"media2-session"},{"artifactId":{"name":"media2-common","group":"androidx.media2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/media2","libraryName":"media2-common"},{"artifactId":{"name":"media2-exoplayer","group":"androidx.media2","version":"+"},"copyrightHolder":"Google Inc","copyrightStatement":"Copyright © Google Inc. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://developer.android.com/jetpack/androidx/releases/media2","libraryName":"media2-exoplayer"},{"artifactId":{"name":"subsampling-scale-image-view-androidx","group":"com.davemorrissey.labs","version":"+"},"copyrightHolder":"David Morrissey and contributors","copyrightStatement":"Copyright © David Morrissey and contributors. All rights reserved.","license":"The Apache Software License, Version 2.0","licenseUrl":"http://www.apache.org/licenses/LICENSE-2.0.txt","normalizedLicense":"apache2","url":"https://github.com/davemorrissey/subsampling-scale-image-view","libraryName":"subsampling-scale-image-view-androidx"},{"artifactId":{"name":"ffmpeg-kit-min","group":"com.arthenica","version":"+"},"copyrightHolder":"Taner Şener","copyrightStatement":"Copyright © Taner Şener. All rights reserved.","license":"GNU Lesser General Public License, Version 3","licenseUrl":"https://www.gnu.org/licenses/lgpl-3.0.txt","normalizedLicense":"lgpl3","url":"https://github.com/tanersener/ffmpeg-kit","libraryName":"ffmpeg-kit-min"},{"artifactId":{"name":"smart-exception-java","group":"com.arthenica","version":"+"},"copyrightHolder":"Taner Şener","copyrightStatement":"Copyright © Taner Şener. All rights reserved.","license":"The 3-Clause BSD License","licenseUrl":"https://opensource.org/licenses/BSD-3-Clause","normalizedLicense":"bsd_3_clauses","url":"https://github.com/tanersener/smart-exception","libraryName":"smart-exception-java"},{"artifactId":{"name":"smart-exception-common","group":"com.arthenica","version":"+"},"copyrightHolder":"Taner Şener","copyrightStatement":"Copyright © Taner Şener. All rights reserved.","license":"The 3-Clause BSD License","licenseUrl":"https://opensource.org/licenses/BSD-3-Clause","normalizedLicense":"bsd_3_clauses","url":"https://github.com/tanersener/smart-exception","libraryName":"smart-exception-common"}]} \ No newline at end of file