Merge branch 'master' into 'profile_perf_fix'

# Conflicts:
#   app/build.gradle
#   app/src/main/java/org/pixeldroid/app/settings/SettingsActivity.kt
#   build.gradle
This commit is contained in:
Matthieu 2022-03-17 22:11:15 +00:00
commit d49e0f58ff
39 changed files with 612 additions and 108 deletions

View File

@ -28,8 +28,8 @@ android {
applicationId "org.pixeldroid.app" applicationId "org.pixeldroid.app"
minSdkVersion 23 minSdkVersion 23
targetSdkVersion 31 targetSdkVersion 31
versionCode 7 versionCode 8
versionName "1.0.beta7" versionName "1.0.beta8"
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
testInstrumentationRunnerArguments clearPackageData: 'true' testInstrumentationRunnerArguments clearPackageData: 'true'
@ -128,6 +128,9 @@ dependencies {
implementation "androidx.activity:activity-ktx:1.4.0" implementation "androidx.activity:activity-ktx:1.4.0"
implementation 'androidx.fragment:fragment-ktx:1.4.1' implementation 'androidx.fragment:fragment-ktx:1.4.1'
implementation 'androidx.work:work-runtime-ktx:2.7.1' implementation 'androidx.work:work-runtime-ktx:2.7.1'
implementation 'androidx.media2:media2-widget:1.2.1'
implementation 'androidx.media2:media2-player:1.2.1'
// Use the most recent version of CameraX // Use the most recent version of CameraX
def cameraX_version = '1.1.0-beta02' def cameraX_version = '1.1.0-beta02'

View File

@ -852,4 +852,106 @@
copyrightHolder: Google Inc copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0 license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: https://developer.android.com/jetpack/androidx/releases/room#2.4.1 url: https://developer.android.com/jetpack/androidx/releases/room#2.4.1
- artifact: androidx.window:window:+
name: window
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/jetpack/androidx/releases/window#1.0.0
- artifact: com.android.support:animated-vector-drawable:+
name: animated-vector-drawable
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:appcompat-v7:+
name: appcompat-v7
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-annotations:+
name: support-annotations
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-compat:+
name: support-compat
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-core-ui:+
name: support-core-ui
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-core-utils:+
name: support-core-utils
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-fragment:+
name: support-fragment
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-media-compat:+
name: support-media-compat
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-v4:+
name: support-v4
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: com.android.support:support-vector-drawable:+
name: support-vector-drawable
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.media2:media2-widget:+
name: media2-widget
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/jetpack/androidx/releases/media2
- artifact: androidx.palette:palette:+
name: palette
copyrightHolder: Google Inc
license: The Apache Software License, Version 2.0
licenseUrl: http://www.apache.org/licenses/LICENSE-2.0.txt
url: http://developer.android.com/tools/extras/support-library.html
- artifact: androidx.media2:media2-player:+
name: media2-player
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/jetpack/androidx/releases/media2
- artifact: androidx.media2:media2-session:+
name: media2-session
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/jetpack/androidx/releases/media2
- artifact: androidx.media2:media2-common:+
name: media2-common
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/jetpack/androidx/releases/media2
- artifact: androidx.media2:media2-exoplayer:+
name: media2-exoplayer
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/jetpack/androidx/releases/media2

View File

@ -44,6 +44,13 @@
public *; public *;
} }
-keepclassmembers class org.pixeldroid.app.settings.licenseObjects.* { *; }
-keep public enum org.pixeldroid.app.settings.licenseObjects.*$** {
**[] $VALUES;
public *;
}
# preserve line numbers for crash reporting # preserve line numbers for crash reporting
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile -renamesourcefileattribute SourceFile

View File

@ -22,6 +22,11 @@
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/AppTheme" android:theme="@style/AppTheme"
tools:replace="android:allowBackup"> tools:replace="android:allowBackup">
<activity
android:name=".posts.MediaViewerActivity"
android:exported="false"
android:configChanges="keyboardHidden|orientation|screenSize"
android:theme="@style/AppTheme.NoActionBar" />
<activity android:name=".postCreation.camera.CameraActivity" /> <activity android:name=".postCreation.camera.CameraActivity" />
<activity <activity
android:name=".posts.ReportActivity" android:name=".posts.ReportActivity"
@ -30,20 +35,38 @@
<activity android:name=".postCreation.photoEdit.PhotoEditActivity" /> <activity android:name=".postCreation.photoEdit.PhotoEditActivity" />
<activity <activity
android:name=".postCreation.PostCreationActivity" android:name=".postCreation.PostCreationActivity"
android:exported="true"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
tools:ignore="LockedOrientationActivity" tools:ignore="LockedOrientationActivity">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND" /> <action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" /> <action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" /> <category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="image/*" /> <data android:mimeType="image/*" />
</intent-filter> </intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.SEND_MULTIPLE" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="video/*" />
</intent-filter>
</activity> </activity>
<activity <activity
android:name=".profile.FollowsActivity" android:name=".profile.FollowsActivity"
@ -69,11 +92,11 @@
tools:ignore="LockedOrientationActivity" /> tools:ignore="LockedOrientationActivity" />
<activity <activity
android:name=".MainActivity" android:name=".MainActivity"
android:exported="true"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/AppTheme.Launcher" android:theme="@style/AppTheme.Launcher"
android:windowSoftInputMode="adjustPan" android:windowSoftInputMode="adjustPan"
tools:ignore="LockedOrientationActivity" tools:ignore="LockedOrientationActivity">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />
@ -86,11 +109,11 @@
</activity> </activity>
<activity <activity
android:name=".LoginActivity" android:name=".LoginActivity"
android:exported="true"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
android:windowSoftInputMode="adjustResize" android:windowSoftInputMode="adjustResize"
tools:ignore="LockedOrientationActivity" tools:ignore="LockedOrientationActivity">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -104,16 +127,16 @@
</activity> </activity>
<activity <activity
android:name="com.yalantis.ucrop.UCropActivity" android:name="com.yalantis.ucrop.UCropActivity"
android:exported="true"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
android:theme="@style/AppTheme.NoActionBar" android:theme="@style/AppTheme.NoActionBar"
tools:ignore="LockedOrientationActivity" tools:ignore="LockedOrientationActivity" />
android:exported="true"/>
<activity <activity
android:name=".searchDiscover.SearchActivity" android:name=".searchDiscover.SearchActivity"
android:exported="true"
android:launchMode="singleTop" android:launchMode="singleTop"
android:screenOrientation="sensorPortrait" android:screenOrientation="sensorPortrait"
tools:ignore="LockedOrientationActivity" tools:ignore="LockedOrientationActivity">
android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.SEARCH" /> <action android:name="android.intent.action.SEARCH" />
</intent-filter> </intent-filter>
@ -142,7 +165,6 @@
android:name="android.support.FILE_PROVIDER_PATHS" android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/file_paths" /> android:resource="@xml/file_paths" />
</provider> </provider>
</application> </application>
</manifest> </manifest>

File diff suppressed because one or more lines are too long

View File

@ -50,11 +50,12 @@ import kotlin.math.ceil
private const val TAG = "Post Creation Activity" private const val TAG = "Post Creation Activity"
data class PhotoData( data class PhotoData(
var imageUri: Uri, var imageUri: Uri,
var size: Long, var size: Long,
var uploadId: String? = null, var uploadId: String? = null,
var progress: Int? = null, var progress: Int? = null,
var imageDescription: String? = null, var imageDescription: String? = null,
var video: Boolean,
) )
class PostCreationActivity : BaseActivity() { class PostCreationActivity : BaseActivity() {
@ -85,7 +86,7 @@ class PostCreationActivity : BaseActivity() {
intent.clipData?.let { addPossibleImages(it) } intent.clipData?.let { addPossibleImages(it) }
val carousel: ImageCarousel = binding.carousel val carousel: ImageCarousel = binding.carousel
carousel.addData(photoData.map { CarouselItem(it.imageUri) }) carousel.addData(photoData.map { CarouselItem(it.imageUri, video = it.video) })
carousel.layoutCarouselCallback = { carousel.layoutCarouselCallback = {
if(it){ if(it){
// Became a carousel // Became a carousel
@ -138,7 +139,7 @@ class PostCreationActivity : BaseActivity() {
binding.removePhotoButton.setOnClickListener { binding.removePhotoButton.setOnClickListener {
carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition -> carousel.currentPosition.takeIf { it != -1 }?.let { currentPosition ->
photoData.removeAt(currentPosition) photoData.removeAt(currentPosition)
carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
binding.addPhotoButton.isEnabled = true binding.addPhotoButton.isEnabled = true
} }
} }
@ -164,16 +165,17 @@ class PostCreationActivity : BaseActivity() {
} }
for (i in 0 until count) { for (i in 0 until count) {
clipData.getItemAt(i).uri.let { clipData.getItemAt(i).uri.let {
val size = it.getSize() val sizeAndVideoPair: Pair<Long, Boolean> = it.getSizeAndVideoValidate()
photoData.add(PhotoData(imageUri = it, size = size)) photoData.add(PhotoData(imageUri = it, size = sizeAndVideoPair.first, video = sizeAndVideoPair.second))
} }
} }
} }
/** /**
* Returns the size of the file of the Uri, and opens a dialog in case it is too big. * 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.getSize(): Long { private fun Uri.getSizeAndVideoValidate(): Pair<Long, Boolean> {
val size: Long = val size: Long =
if (toString().startsWith("content")) { if (toString().startsWith("content")) {
contentResolver.query(this, null, null, null, null) contentResolver.query(this, null, null, null, null)
@ -191,22 +193,24 @@ class PostCreationActivity : BaseActivity() {
} }
val sizeInkBytes = ceil(size.toDouble() / 1000).toLong() 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) { if (sizeInkBytes > instance.maxPhotoSize || sizeInkBytes > instance.maxVideoSize) {
val maxSize = when { val maxSize = if (isVideo) instance.maxVideoSize else instance.maxPhotoSize
instance.maxPhotoSize != instance.maxVideoSize -> {
val type = contentResolver.getType(this)
if (type?.startsWith("video/") == true) {
instance.maxVideoSize
} else instance.maxPhotoSize
}
else -> instance.maxPhotoSize
}
AlertDialog.Builder(this@PostCreationActivity).apply { 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, photoData.size + 1, sizeInkBytes, maxSize))
setNegativeButton(android.R.string.ok) { _, _ -> } setNegativeButton(android.R.string.ok) { _, _ -> }
}.show() }.show()
} }
return size return Pair(size, isVideo)
} }
private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> private val addPhotoResultContract = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
@ -214,7 +218,7 @@ class PostCreationActivity : BaseActivity() {
result.data?.clipData?.let { result.data?.clipData?.let {
addPossibleImages(it) addPossibleImages(it)
} }
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
} else if (result.resultCode != Activity.RESULT_CANCELED) { } else if (result.resultCode != Activity.RESULT_CANCELED) {
Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show() Toast.makeText(applicationContext, "Error while adding images", Toast.LENGTH_SHORT).show()
} }
@ -306,8 +310,8 @@ class PostCreationActivity : BaseActivity() {
*/ */
private fun upload() { private fun upload() {
enableButton(false) enableButton(false)
binding.uploadProgressBar.visibility = View.VISIBLE binding.uploadProgressBar.visibility = VISIBLE
binding.uploadCompletedTextview.visibility = View.INVISIBLE binding.uploadCompletedTextview.visibility = INVISIBLE
binding.removePhotoButton.isEnabled = false binding.removePhotoButton.isEnabled = false
binding.editPhotoButton.isEnabled = false binding.editPhotoButton.isEnabled = false
binding.addPhotoButton.isEnabled = false binding.addPhotoButton.isEnabled = false
@ -429,21 +433,30 @@ class PostCreationActivity : BaseActivity() {
val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0) val position: Int = result.data!!.getIntExtra(PhotoEditActivity.PICTURE_POSITION, 0)
photoData.getOrNull(position)?.apply { photoData.getOrNull(position)?.apply {
imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri() imageUri = result.data!!.getStringExtra(PhotoEditActivity.PICTURE_URI)!!.toUri()
size = imageUri.getSize() val (imageSize, imageVideo) = imageUri.getSizeAndVideoValidate()
size = imageSize
video = imageVideo
progress = null progress = null
uploadId = null uploadId = null
} ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() } ?: Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription) }) binding.carousel.addData(photoData.map { CarouselItem(it.imageUri, it.imageDescription, it.video) })
} else if(result?.resultCode != Activity.RESULT_CANCELED){ } else if(result?.resultCode != Activity.RESULT_CANCELED){
Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show() Toast.makeText(applicationContext, "Error while editing", Toast.LENGTH_SHORT).show()
} }
} }
private fun edit(position: Int) { private fun edit(position: Int) {
val intent = Intent(this, PhotoEditActivity::class.java) if(photoData[position].video){
.putExtra(PhotoEditActivity.PICTURE_URI, photoData[position].imageUri) AlertDialog.Builder(this).apply {
.putExtra(PhotoEditActivity.PICTURE_POSITION, position) setMessage(R.string.video_edit_not_yet_supported)
editResultContract.launch(intent) setNegativeButton(android.R.string.ok) { _, _ -> }
}.show()
} else {
val intent = Intent(this, PhotoEditActivity::class.java)
.putExtra(PhotoEditActivity.PICTURE_URI, photoData[position].imageUri)
.putExtra(PhotoEditActivity.PICTURE_POSITION, position)
editResultContract.launch(intent)
}
} }
} }

View File

@ -2,10 +2,11 @@ package org.pixeldroid.app.postCreation
import android.content.Context import android.content.Context
import android.util.AttributeSet import android.util.AttributeSet
import android.widget.FrameLayout
import android.widget.RelativeLayout import android.widget.RelativeLayout
internal class SquareLayout(context: Context, attrs: AttributeSet) : internal class SquareLayout(context: Context, attrs: AttributeSet) :
RelativeLayout(context, attrs) { FrameLayout(context, attrs) {
override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
super.onMeasure(widthMeasureSpec, widthMeasureSpec) super.onMeasure(widthMeasureSpec, widthMeasureSpec)

View File

@ -41,6 +41,7 @@ import kotlin.math.max
import kotlin.math.min import kotlin.math.min
import kotlin.properties.Delegates import kotlin.properties.Delegates
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.utils.BaseFragment
private const val ANIMATION_FAST_MILLIS = 50L private const val ANIMATION_FAST_MILLIS = 50L
private const val ANIMATION_SLOW_MILLIS = 100L private const val ANIMATION_SLOW_MILLIS = 100L
@ -48,7 +49,7 @@ private const val ANIMATION_SLOW_MILLIS = 100L
/** /**
* Camera fragment * Camera fragment
*/ */
class CameraFragment : Fragment() { class CameraFragment : BaseFragment() {
private lateinit var container: ConstraintLayout private lateinit var container: ConstraintLayout
@ -314,10 +315,15 @@ class CameraFragment : Fragment() {
} }
private fun setupUploadImage() { private fun setupUploadImage() {
val videoEnabled: Boolean = db.instanceDao().getInstance(db.userDao().getActiveUser()!!.instance_uri).videoEnabled
var mimeTypes: Array<String> = arrayOf("image/*")
if(videoEnabled) mimeTypes += "video/*"
// Listener for button used to view the most recent photo // Listener for button used to view the most recent photo
binding.photoViewButton.setOnClickListener { binding.photoViewButton.setOnClickListener {
Intent().apply { Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*" type = "*/*"
putExtra(Intent.EXTRA_MIME_TYPES, mimeTypes)
action = Intent.ACTION_GET_CONTENT action = Intent.ACTION_GET_CONTENT
addCategory(Intent.CATEGORY_OPENABLE) addCategory(Intent.CATEGORY_OPENABLE)
putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true)

View File

@ -3,13 +3,17 @@ package org.pixeldroid.app.postCreation.carousel
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.View import android.view.View
import android.view.View.GONE
import android.view.View.VISIBLE
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageButton
import android.widget.ImageView import android.widget.ImageView
import androidx.annotation.IdRes import androidx.annotation.IdRes
import androidx.annotation.LayoutRes import androidx.annotation.LayoutRes
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import com.bumptech.glide.Glide import com.bumptech.glide.Glide
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.posts.MediaViewerActivity
class CarouselAdapter( class CarouselAdapter(
@ -26,6 +30,8 @@ class CarouselAdapter(
class MyViewHolder(itemView: View, imageViewId: Int) : RecyclerView.ViewHolder(itemView) { class MyViewHolder(itemView: View, imageViewId: Int) : RecyclerView.ViewHolder(itemView) {
var img: ImageView = itemView.findViewById(imageViewId) var img: ImageView = itemView.findViewById(imageViewId)
// Null if not relevant
val videoIndicator: ImageButton? = itemView.findViewById(R.id.videoIndicator)
} }
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder { override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): MyViewHolder {
@ -60,10 +66,21 @@ class CarouselAdapter(
} }
override fun onBindViewHolder(holder: MyViewHolder, position: Int) { override fun onBindViewHolder(holder: MyViewHolder, position: Int) {
if(carousel) { if (carousel) {
holder.img.scaleType = imageScaleType holder.img.scaleType = imageScaleType
holder.videoIndicator?.setOnClickListener{
with(dataList[position]) {
MediaViewerActivity.openActivity(
holder.itemView.context,
imageUrl.toString(),
caption
)
}
}
} }
holder.videoIndicator?.visibility = if (dataList[position].video) VISIBLE else GONE
dataList.elementAtOrNull(position)?.let { dataList.elementAtOrNull(position)?.let {
Glide.with(holder.itemView.context) Glide.with(holder.itemView.context)
.load(it.imageUrl) .load(it.imageUrl)
@ -83,7 +100,6 @@ class CarouselAdapter(
true true
} }
} }
} }

View File

@ -4,7 +4,6 @@ import android.net.Uri
data class CarouselItem constructor( data class CarouselItem constructor(
val imageUrl: Uri, val imageUrl: Uri,
val caption: String? = null val caption: String? = null,
) { val video: Boolean
constructor(imageUrl: Uri) : this(imageUrl, null) )
}

View File

@ -49,7 +49,7 @@ class ImageCarousel(
var indicator: CircleIndicator2? = null var indicator: CircleIndicator2? = null
set(newIndicator) { set(newIndicator) {
indicator?.apply { indicator?.apply {
// if we remove it form the view, then the caption textView constraint won't work. // if we remove it from the view, then the caption textView constraint won't work.
this.visibility = View.GONE this.visibility = View.GONE
isBuiltInIndicator = false isBuiltInIndicator = false

View File

@ -0,0 +1,102 @@
package org.pixeldroid.app.posts
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.media.AudioManager.STREAM_MUSIC
import android.os.Bundle
import androidx.core.net.toUri
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.WindowInsetsControllerCompat
import androidx.media.AudioAttributesCompat
import androidx.media2.common.MediaMetadata
import androidx.media2.common.UriMediaItem
import androidx.media2.player.MediaPlayer
import org.pixeldroid.app.databinding.ActivityMediaviewerBinding
import org.pixeldroid.app.utils.BaseActivity
class MediaViewerActivity : BaseActivity() {
private lateinit var mediaPlayer: MediaPlayer
private lateinit var binding: ActivityMediaviewerBinding
companion object {
const val VIDEO_URL_TAG = "video_url_mediavieweractivity"
const val VIDEO_DESCRIPTION_TAG = "video_description_mediavieweractivity"
fun openActivity(context: Context, url: String?, description: String?){
val intent = Intent(context, MediaViewerActivity::class.java)
intent.putExtra(VIDEO_URL_TAG, url)
intent.putExtra(VIDEO_DESCRIPTION_TAG, description)
context.startActivity(intent)
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMediaviewerBinding.inflate(layoutInflater)
setContentView(binding.root)
val uri: String = intent.getStringExtra(VIDEO_URL_TAG).orEmpty()
val description: String? = intent.getStringExtra(VIDEO_DESCRIPTION_TAG)
val mediaItem: UriMediaItem = UriMediaItem.Builder(uri.toUri()).build()
mediaItem.metadata = MediaMetadata.Builder()
.putString(MediaMetadata.METADATA_KEY_TITLE, description ?: "")
.build()
mediaPlayer = MediaPlayer(this)
mediaPlayer.setMediaItem(mediaItem)
binding.videoView.mediaControlView?.setOnFullScreenListener{ view, fullscreen ->
val windowInsetsController = ViewCompat.getWindowInsetsController(window.decorView)
if (!fullscreen) {
// Configure the behavior of the hidden system bars
windowInsetsController?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
windowInsetsController?.show(WindowInsetsCompat.Type.systemBars())
supportActionBar?.show()
requestedOrientation = ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
} else {
// Configure the behavior of the hidden system bars
windowInsetsController?.systemBarsBehavior = WindowInsetsControllerCompat.BEHAVIOR_SHOW_TRANSIENT_BARS_BY_SWIPE
// Hide both the status bar and the navigation bar
windowInsetsController?.hide(WindowInsetsCompat.Type.systemBars())
requestedOrientation =
if (mediaPlayer.videoSize.height < mediaPlayer.videoSize.width) {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
} else {
ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
}
supportActionBar?.hide()
}
}
// Configure audio
mediaPlayer.setAudioAttributes(AudioAttributesCompat.Builder()
.setLegacyStreamType(STREAM_MUSIC)
.setUsage(AudioAttributesCompat.USAGE_MEDIA)
.setContentType(AudioAttributesCompat.CONTENT_TYPE_MOVIE)
.build()
)
mediaPlayer.prepare()
binding.videoView.setPlayer(mediaPlayer)
// Start actually playing the video
mediaPlayer.play()
}
override fun onPause() {
super.onPause()
mediaPlayer.pause()
}
override fun onDestroy() {
super.onDestroy()
mediaPlayer.close()
}
}

View File

@ -37,6 +37,9 @@ import com.karumi.dexter.listener.PermissionDeniedResponse
import com.karumi.dexter.listener.PermissionGrantedResponse import com.karumi.dexter.listener.PermissionGrantedResponse
import com.karumi.dexter.listener.single.BasePermissionListener import com.karumi.dexter.listener.single.BasePermissionListener
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.posts.MediaViewerActivity.Companion.VIDEO_DESCRIPTION_TAG
import org.pixeldroid.app.posts.MediaViewerActivity.Companion.VIDEO_URL_TAG
import org.pixeldroid.app.posts.MediaViewerActivity.Companion.openActivity
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -594,6 +597,7 @@ private class AlbumViewPagerAdapter(private val media_attachments: List<Attachme
override fun getItemCount() = media_attachments.size override fun getItemCount() = media_attachments.size
override fun onBindViewHolder(holder: ViewHolder, position: Int) { override fun onBindViewHolder(holder: ViewHolder, position: Int) {
media_attachments[position].apply { media_attachments[position].apply {
val video = type == Attachment.AttachmentType.video
val blurhashBitMap = blurhash?.let { val blurhashBitMap = blurhash?.let {
BlurHashDecoder.blurHashBitmap( BlurHashDecoder.blurHashBitmap(
holder.binding.root.resources, holder.binding.root.resources,
@ -603,16 +607,28 @@ private class AlbumViewPagerAdapter(private val media_attachments: List<Attachme
) )
} }
if (sensitive == false) { if (sensitive == false) {
val imageUrl = if(video) preview_url else url
Glide.with(holder.binding.root) Glide.with(holder.binding.root)
.asDrawable().fitCenter() .asDrawable().fitCenter()
.placeholder(blurhashBitMap) .placeholder(blurhashBitMap)
.load(url).into(holder.image) .load(imageUrl).into(holder.image)
} else { } else {
Glide.with(holder.binding.root) Glide.with(holder.binding.root)
.asDrawable().fitCenter() .asDrawable().fitCenter()
.load(blurhashBitMap).into(holder.image) .load(blurhashBitMap).into(holder.image)
} }
holder.videoPlayButton.visibility = if(video) View.VISIBLE else View.GONE
if(video){
holder.videoPlayButton.setOnClickListener {
openActivity(holder.binding.root.context, url, description)
}
holder.image.setOnClickListener {
openActivity(holder.binding.root.context, url, description)
}
}
val description = description val description = description
.orEmpty() .orEmpty()
.ifEmpty { holder.binding.root.context.getString(R.string.no_description) } .ifEmpty { holder.binding.root.context.getString(R.string.no_description) }
@ -633,5 +649,6 @@ private class AlbumViewPagerAdapter(private val media_attachments: List<Attachme
class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){ class ViewHolder(val binding: AlbumImageViewBinding) : RecyclerView.ViewHolder(binding.root){
val image: ImageView = binding.imageImageView val image: ImageView = binding.imageImageView
val videoPlayButton: ImageView = binding.videoPlayButton
} }
} }

View File

@ -9,6 +9,7 @@ import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import android.widget.ImageView import android.widget.ImageView
import android.widget.TextView import android.widget.TextView
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.ViewModelProvider import androidx.lifecycle.ViewModelProvider
@ -31,6 +32,7 @@ import org.pixeldroid.app.utils.api.objects.Account
import org.pixeldroid.app.utils.api.objects.Notification import org.pixeldroid.app.utils.api.objects.Notification
import org.pixeldroid.app.utils.api.objects.Status import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.utils.di.PixelfedAPIHolder import org.pixeldroid.app.utils.di.PixelfedAPIHolder
import org.pixeldroid.app.utils.notificationsWorker.makeChannelGroupId
/** /**
@ -64,6 +66,16 @@ class NotificationsFragment : CachedFeedFragment<Notification>() {
} }
override fun onResume() {
super.onResume()
with(NotificationManagerCompat.from(requireContext())) {
// Cancel account notification group
db.userDao().getActiveUser()?.let {
cancel( makeChannelGroupId(it).hashCode())
}
}
}
/** /**
* View Holder for a [Notification] RecyclerView list item. * View Holder for a [Notification] RecyclerView list item.
*/ */

View File

@ -38,6 +38,7 @@ import org.pixeldroid.app.utils.db.entities.UserDatabaseEntity
import org.pixeldroid.app.utils.openUrl import org.pixeldroid.app.utils.openUrl
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import org.pixeldroid.app.utils.api.objects.Attachment
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -340,6 +341,7 @@ class ProfileViewModelFactory @ExperimentalPagingApi constructor(
class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerView.ViewHolder(binding.root) { class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerView.ViewHolder(binding.root) {
private val postPreview: ImageView = binding.postPreview private val postPreview: ImageView = binding.postPreview
private val albumIcon: ImageView = binding.albumIcon private val albumIcon: ImageView = binding.albumIcon
private val videoIcon: ImageView = binding.videoIcon
fun bind(post: Status) { fun bind(post: Status) {
@ -352,11 +354,14 @@ class ProfilePostsViewHolder(binding: FragmentProfilePostsBinding) : RecyclerVie
} else { } else {
ImageConverter.setSquareImageFromURL(itemView, post.getPostPreviewURL(), postPreview) ImageConverter.setSquareImageFromURL(itemView, post.getPostPreviewURL(), postPreview)
} }
if(post.media_attachments?.size ?: 0 > 1) { if(post.media_attachments?.size ?: 0 > 1) {
albumIcon.visibility = View.VISIBLE albumIcon.visibility = View.VISIBLE
} else { } else {
albumIcon.visibility = View.GONE albumIcon.visibility = View.GONE
if(post.media_attachments?.get(0)?.type == Attachment.AttachmentType.video) {
videoIcon.visibility = View.VISIBLE
} else videoIcon.visibility = View.GONE
} }
postPreview.setOnClickListener { postPreview.setOnClickListener {

View File

@ -8,4 +8,5 @@ import org.pixeldroid.app.R
class ProfilePostViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) { class ProfilePostViewHolder(val postView: View) : RecyclerView.ViewHolder(postView) {
val postPreview: ImageView = postView.findViewById(R.id.postPreview) val postPreview: ImageView = postView.findViewById(R.id.postPreview)
val albumIcon: ImageView = postView.findViewById(R.id.albumIcon) val albumIcon: ImageView = postView.findViewById(R.id.albumIcon)
val videoIcon: ImageView = postView.findViewById(R.id.albumIcon)
} }

View File

@ -19,6 +19,7 @@ import org.pixeldroid.app.utils.api.objects.Status
import org.pixeldroid.app.posts.PostActivity import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.utils.BaseFragment import org.pixeldroid.app.utils.BaseFragment
import org.pixeldroid.app.utils.ImageConverter import org.pixeldroid.app.utils.ImageConverter
import org.pixeldroid.app.utils.api.objects.Attachment
import org.pixeldroid.app.utils.bindingLifecycleAware import org.pixeldroid.app.utils.bindingLifecycleAware
import retrofit2.HttpException import retrofit2.HttpException
import java.io.IOException import java.io.IOException
@ -120,6 +121,10 @@ class SearchDiscoverFragment : BaseFragment() {
holder.albumIcon.visibility = View.VISIBLE holder.albumIcon.visibility = View.VISIBLE
} else { } else {
holder.albumIcon.visibility = View.GONE holder.albumIcon.visibility = View.GONE
if(post?.media_attachments?.get(0)?.type == Attachment.AttachmentType.video) {
holder.videoIcon.visibility = View.VISIBLE
} else holder.videoIcon.visibility = View.GONE
} }
ImageConverter.setSquareImageFromURL(holder.postView, post?.getPostPreviewURL(), holder.postPreview, post?.media_attachments?.firstOrNull()?.blurhash) ImageConverter.setSquareImageFromURL(holder.postView, post?.getPostPreviewURL(), holder.postPreview, post?.media_attachments?.firstOrNull()?.blurhash)
holder.postPreview.setOnClickListener { holder.postPreview.setOnClickListener {

View File

@ -4,7 +4,10 @@ import android.os.Bundle
import com.google.gson.Gson import com.google.gson.Gson
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.databinding.OpenSourceLicenseBinding import org.pixeldroid.app.databinding.OpenSourceLicenseBinding
import org.pixeldroid.app.settings.licenseObjects.Libraries
import org.pixeldroid.app.settings.licenseObjects.OpenSourceItem
import org.pixeldroid.app.utils.BaseActivity import org.pixeldroid.app.utils.BaseActivity
import java.io.FileNotFoundException
/** /**
* Displays licenses for all app dependencies. JSON is * Displays licenses for all app dependencies. JSON is
@ -30,13 +33,10 @@ class LicenseActivity: BaseActivity() {
private fun setupRecyclerView() { private fun setupRecyclerView() {
val text: String = applicationContext.assets.open("licenses.json") val text: String = applicationContext.assets.open("licenses.json")
.bufferedReader().use { it.readText() } .bufferedReader().use { it.readText() }
val listObj: List<OpenSourceItem> = Gson().fromJson(text, Libraries::class.java).libraries val listObj: List<OpenSourceItem> = Gson().fromJson(text, Libraries::class.java).libraries
val adapter = OpenSourceLicenseAdapter() val adapter = OpenSourceLicenseAdapter(listObj)
binding.openSourceLicenseRecyclerView.adapter = adapter binding.openSourceLicenseRecyclerView.adapter = adapter
adapter.updateList(listObj)
} }
} }

View File

@ -7,18 +7,11 @@ import android.view.ViewGroup
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import org.pixeldroid.app.databinding.OpenSourceItemBinding import org.pixeldroid.app.databinding.OpenSourceItemBinding
import org.pixeldroid.app.settings.licenseObjects.OpenSourceItem
class OpenSourceLicenseAdapter : class OpenSourceLicenseAdapter(private val openSourceItems: List<OpenSourceItem>) :
RecyclerView.Adapter<OpenSourceLicenseAdapter.OpenSourceLicenceViewHolder>() { RecyclerView.Adapter<OpenSourceLicenseAdapter.OpenSourceLicenceViewHolder>() {
private var openSourceItems: List<OpenSourceItem> = emptyList()
@SuppressLint("NotifyDataSetChanged")
fun updateList(newOpenSourceItems: List<OpenSourceItem>) {
openSourceItems = newOpenSourceItems
notifyDataSetChanged()
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OpenSourceLicenceViewHolder override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): OpenSourceLicenceViewHolder
{ {
val itemBinding = OpenSourceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false) val itemBinding = OpenSourceItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
@ -67,15 +60,4 @@ class OpenSourceLicenseAdapter :
} }
} }
} }
} }
data class OpenSourceItem(
val libraryName: String?,
val copyrightHolder: String?,
val url: String?,
val license: String?,
val licenseUrl: String?,
)
data class Libraries(
val libraries: List<OpenSourceItem>
)

View File

@ -0,0 +1,5 @@
package org.pixeldroid.app.settings.licenseObjects
data class Libraries(
val libraries: List<OpenSourceItem>
)

View File

@ -0,0 +1,10 @@
package org.pixeldroid.app.settings.licenseObjects
data class OpenSourceItem(
val libraryName: String?,
val copyrightHolder: String?,
val url: String?,
val license: String?,
val licenseUrl: String?,
)

View File

@ -73,6 +73,7 @@ fun bitmapFromUri(contentResolver: ContentResolver, uri: Uri?): Bitmap =
) )
{ decoder, _, _ -> decoder.isMutableRequired = true } { decoder, _, _ -> decoder.isMutableRequired = true }
} else { } else {
@Suppress("DEPRECATION")
val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri) val bitmap = MediaStore.Images.Media.getBitmap(contentResolver, uri)
modifyOrientation(bitmap!!, contentResolver, uri!!) modifyOrientation(bitmap!!, contentResolver, uri!!)
} }

View File

@ -22,7 +22,7 @@ import org.pixeldroid.app.utils.api.objects.Notification
PublicFeedStatusDatabaseEntity::class, PublicFeedStatusDatabaseEntity::class,
Notification::class Notification::class
], ],
version = 4 version = 5
) )
@TypeConverters(Converters::class) @TypeConverters(Converters::class)
abstract class AppDatabase : RoomDatabase() { abstract class AppDatabase : RoomDatabase() {
@ -39,4 +39,9 @@ val MIGRATION_3_4 = object : Migration(3, 4) {
database.execSQL("DELETE FROM publicPosts") database.execSQL("DELETE FROM publicPosts")
database.execSQL("DELETE FROM notifications") database.execSQL("DELETE FROM notifications")
} }
}
val MIGRATION_4_5 = object : Migration(4, 5) {
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE instances ADD COLUMN videoEnabled INTEGER NOT NULL DEFAULT 1")
}
} }

View File

@ -9,6 +9,7 @@ import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEF
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_PHOTO_SIZE import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_PHOTO_SIZE
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_TOOT_CHARS
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_VIDEO_SIZE import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_MAX_VIDEO_SIZE
import org.pixeldroid.app.utils.db.entities.InstanceDatabaseEntity.Companion.DEFAULT_VIDEO_ENABLED
import org.pixeldroid.app.utils.normalizeDomain import org.pixeldroid.app.utils.normalizeDomain
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
@ -33,13 +34,14 @@ fun addUser(db: AppDatabase, account: Account, instance_uri: String, activeUser:
fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) { fun storeInstance(db: AppDatabase, nodeInfo: NodeInfo?, instance: Instance? = null) {
val dbInstance: InstanceDatabaseEntity = nodeInfo?.run { val dbInstance: InstanceDatabaseEntity = nodeInfo?.run {
InstanceDatabaseEntity( InstanceDatabaseEntity(
uri = normalizeDomain(metadata?.config?.site?.url!!), uri = normalizeDomain(metadata?.config?.site?.url!!),
title = metadata.config.site.name!!, title = metadata.config.site.name!!,
maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(), maxStatusChars = metadata.config.uploader?.max_caption_length!!.toInt(),
maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE, maxPhotoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_PHOTO_SIZE,
//Pixelfed doesn't distinguish between max photo and video size // Pixelfed doesn't distinguish between max photo and video size
maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE, maxVideoSize = metadata.config.uploader.max_photo_size?.toIntOrNull() ?: DEFAULT_MAX_VIDEO_SIZE,
albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT albumLimit = metadata.config.uploader.album_limit?.toIntOrNull() ?: DEFAULT_ALBUM_LIMIT,
videoEnabled = metadata.config.features?.video ?: DEFAULT_VIDEO_ENABLED
) )
} ?: instance?.run { } ?: instance?.run {
InstanceDatabaseEntity( InstanceDatabaseEntity(

View File

@ -8,6 +8,9 @@ interface InstanceDao {
@Query("SELECT * FROM instances") @Query("SELECT * FROM instances")
fun getAll(): List<InstanceDatabaseEntity> fun getAll(): List<InstanceDatabaseEntity>
@Query("SELECT * FROM instances WHERE uri=:instanceUri LIMIT 1")
fun getInstance(instanceUri: String): InstanceDatabaseEntity
/** /**
* Insert an instance, if it already exists return -1 * Insert an instance, if it already exists return -1
*/ */

View File

@ -14,6 +14,8 @@ data class InstanceDatabaseEntity (
var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE, var maxVideoSize: Int = DEFAULT_MAX_VIDEO_SIZE,
// How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4 // How many photos can go into an album. Default limit for Pixelfed and Mastodon is 4
var albumLimit: Int = DEFAULT_ALBUM_LIMIT, var albumLimit: Int = DEFAULT_ALBUM_LIMIT,
// Is video functionality enabled on this instance?
var videoEnabled: Boolean = DEFAULT_VIDEO_ENABLED,
) { ) {
companion object{ companion object{
// Default max number of chars for Mastodon: used when their is no other value supplied by // Default max number of chars for Mastodon: used when their is no other value supplied by
@ -23,5 +25,6 @@ data class InstanceDatabaseEntity (
const val DEFAULT_MAX_PHOTO_SIZE = 8000 const val DEFAULT_MAX_PHOTO_SIZE = 8000
const val DEFAULT_MAX_VIDEO_SIZE = 40000 const val DEFAULT_MAX_VIDEO_SIZE = 40000
const val DEFAULT_ALBUM_LIMIT = 4 const val DEFAULT_ALBUM_LIMIT = 4
const val DEFAULT_VIDEO_ENABLED = true
} }
} }

View File

@ -6,6 +6,7 @@ import org.pixeldroid.app.utils.db.AppDatabase
import dagger.Module import dagger.Module
import dagger.Provides import dagger.Provides
import org.pixeldroid.app.utils.db.MIGRATION_3_4 import org.pixeldroid.app.utils.db.MIGRATION_3_4
import org.pixeldroid.app.utils.db.MIGRATION_4_5
import javax.inject.Singleton import javax.inject.Singleton
@Module @Module
@ -17,6 +18,7 @@ class DatabaseModule(private val context: Context) {
return Room.databaseBuilder( return Room.databaseBuilder(
context, context,
AppDatabase::class.java, "pixeldroid" AppDatabase::class.java, "pixeldroid"
).addMigrations(MIGRATION_3_4).allowMainThreadQueries().build() ).addMigrations(MIGRATION_3_4).addMigrations(MIGRATION_4_5)
.allowMainThreadQueries().build()
} }
} }

View File

@ -15,6 +15,7 @@ import androidx.work.WorkerParameters
import org.pixeldroid.app.MainActivity import org.pixeldroid.app.MainActivity
import org.pixeldroid.app.R import org.pixeldroid.app.R
import org.pixeldroid.app.posts.PostActivity import org.pixeldroid.app.posts.PostActivity
import org.pixeldroid.app.posts.fromHtml
import org.pixeldroid.app.utils.PixelDroidApplication import org.pixeldroid.app.utils.PixelDroidApplication
import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser import org.pixeldroid.app.utils.api.PixelfedAPI.Companion.apiForUser
import org.pixeldroid.app.utils.api.objects.Notification import org.pixeldroid.app.utils.api.objects.Notification
@ -71,8 +72,7 @@ class NotificationsWorker(
) )
while (!newNotifications.isNullOrEmpty() while (!newNotifications.isNullOrEmpty()
&& newNotifications.map { it.created_at ?: Instant.MIN } && newNotifications.maxOf { it.created_at ?: Instant.MIN } > previouslyLatestNotification?.created_at ?: Instant.MIN
.maxOrNull()!! > previouslyLatestNotification?.created_at ?: Instant.MIN
) { ) {
// Add to db // Add to db
val filteredNewNotifications: List<Notification> = newNotifications.filter { val filteredNewNotifications: List<Notification> = newNotifications.filter {
@ -83,6 +83,12 @@ class NotificationsWorker(
db.notificationDao().insertAll(filteredNewNotifications) db.notificationDao().insertAll(filteredNewNotifications)
//If multiple notifications, show summary of them
if(filteredNewNotifications.size > 1){
showNotificationSummary(filteredNewNotifications, uniqueUserId)
}
// Launch new notifications // Launch new notifications
filteredNewNotifications.forEach { filteredNewNotifications.forEach {
showNotification(it, user, uniqueUserId) showNotification(it, user, uniqueUserId)
@ -106,6 +112,39 @@ class NotificationsWorker(
return Result.success() return Result.success()
} }
private fun showNotificationSummary(notifications: List<Notification>, uniqueUserId: String) {
val content = joinNames(
applicationContext,
notifications.mapNotNull { it.account?.getDisplayName() }
)
val title: String = applicationContext.resources.getQuantityString(
R.plurals.notification_title_summary,
notifications.size,
notifications.size
)
val groupBuilder = NotificationCompat.Builder(applicationContext, makeChannelId(uniqueUserId, null))
.setContentTitle(title)
.setContentText(content)
.setGroupSummary(true)
.setAutoCancel(true)
.setGroup(uniqueUserId)
.setSmallIcon(R.drawable.notification_icon)
.setStyle(NotificationCompat.BigTextStyle().bigText(content))
.setContentIntent(
PendingIntent.getActivity(applicationContext, 0, Intent(applicationContext, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
putExtra(SHOW_NOTIFICATION_TAG, true)
}, PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT)
)
with(NotificationManagerCompat.from(applicationContext)) {
notify(uniqueUserId.hashCode(), groupBuilder.build())
}
}
private fun showNotification( private fun showNotification(
notification: Notification, notification: Notification,
user: UserDatabaseEntity, user: UserDatabaseEntity,
@ -166,7 +205,7 @@ class NotificationsWorker(
.setAutoCancel(true) .setAutoCancel(true)
if (notification.type == mention || notification.type == comment || notification.type == poll){ if (notification.type == mention || notification.type == comment || notification.type == poll){
builder.setContentText(notification.status?.content) builder.setContentText(notification.status?.content?.let { fromHtml(it) })
} }
builder.setGroup(uniqueUserId) builder.setGroup(uniqueUserId)
@ -237,7 +276,8 @@ fun makeNotificationChannels(context: Context, handle: String, channelGroupId: S
/** /**
* [channelGroupId] is the id used to uniquely identify the group: for us it is a unique id * [channelGroupId] is the id used to uniquely identify the group: for us it is a unique id
* identifying a user consisting of the concatenation of the instance uri and user id. * identifying a user consisting of the concatenation of the instance uri and user id
* (see [makeChannelGroupId]).
*/ */
private fun makeChannelId(channelGroupId: String, type: Notification.NotificationType?): String = private fun makeChannelId(channelGroupId: String, type: Notification.NotificationType?): String =
(channelGroupId + (type ?: NotificationsWorker.otherNotificationType)).hashCode().toString() (channelGroupId + (type ?: NotificationsWorker.otherNotificationType)).hashCode().toString()
@ -264,4 +304,27 @@ fun removeNotificationChannelsFromAccount(context: Context, user: UserDatabaseEn
} }
} }
} }
}
/**
* BidiFormatter.unicodeWrap is insufficient in some cases (see Tusky#1921)
* So we force isolation manually
* https://unicode.org/reports/tr9/#Explicit_Directional_Isolates
*/
fun CharSequence.unicodeWrap(): String = "\u2068${this}\u2069"
private fun joinNames(context: Context, notifications: List<String>): String {
return when {
notifications.size > 3 -> {
context.getString(R.string.notification_summary_large).format(
*notifications.subList(0, 3).map { it.unicodeWrap() }.toTypedArray(),
notifications.size - 3
)
}
else -> context.getString( when(notifications.size) {
2 -> R.string.notification_summary_small
else /* ==3 */-> R.string.notification_summary_medium
}).format(*notifications.map { it.unicodeWrap() }.toTypedArray())
}
} }

View File

@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<group
android:scaleX="4"
android:scaleY="4"
android:translateX="-53"
android:translateY="-53">
<path
android:pathData="M23.0332,30.2725L27.5781,30.2725C31.8595,30.2725 35.3302,26.9088 35.3302,22.7596C35.3302,18.6103 31.8595,15.2467 27.5781,15.2467L21.0185,15.2467C18.5485,15.2467 16.5461,17.1872 16.5461,19.581L16.5461,36.451L23.0332,30.2725Z"
android:strokeWidth="1"
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:strokeColor="#00000000"/>
</group>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="@android:color/white"
android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM10,16.5v-9l6,4.5 -6,4.5z"/>
</vector>

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
<androidx.media2.widget.VideoView
android:id="@+id/videoView"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#000000" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -10,6 +10,7 @@
android:id="@+id/addPhotoSquare" android:id="@+id/addPhotoSquare"
android:layout_width="50dp" android:layout_width="50dp"
android:layout_height="50dp" android:layout_height="50dp"
android:layout_gravity="center"
android:layout_centerInParent="true" android:layout_centerInParent="true"
android:layout_centerVertical="true" android:layout_centerVertical="true"
android:background="@drawable/add_photo_button" android:background="@drawable/add_photo_button"

View File

@ -14,4 +14,15 @@
android:adjustViewBounds="true" android:adjustViewBounds="true"
tools:ignore="ContentDescription" /> tools:ignore="ContentDescription" />
<ImageButton
android:id="@+id/videoPlayButton"
android:visibility="gone"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/play_circle_filled"
android:contentDescription="@string/play_video" />
</FrameLayout> </FrameLayout>

View File

@ -38,9 +38,24 @@
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible" tools:visibility="gone"
android:contentDescription="@string/post_is_album" /> android:contentDescription="@string/post_is_album" />
<ImageView
android:id="@+id/videoIcon"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:foreground="@drawable/play_circle_filled"
android:foregroundGravity="center"
android:foregroundTint="#FFFFFF"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:visibility="visible"
android:contentDescription="@string/post_is_video" />
</androidx.constraintlayout.widget.ConstraintLayout> </androidx.constraintlayout.widget.ConstraintLayout>
</androidx.gridlayout.widget.GridLayout> </androidx.gridlayout.widget.GridLayout>

View File

@ -1,15 +1,29 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<org.pixeldroid.app.postCreation.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android" <org.pixeldroid.app.postCreation.SquareLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:foreground="?selectableItemBackground"
android:clickable="true" android:clickable="true"
android:focusable="true"> android:focusable="true"
android:foreground="?selectableItemBackground">
<ImageView <ImageView
android:id="@+id/galleryImage" android:id="@+id/galleryImage"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:contentDescription="@string/post_image"
android:padding="8dp" android:padding="8dp"
android:scaleType="centerCrop" android:scaleType="centerCrop" />
android:contentDescription="@string/post_image" />
<ImageButton
android:id="@+id/videoIndicator"
android:layout_width="24dp"
android:layout_height="24dp"
android:layout_gravity="top"
android:layout_margin="12dp"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/play_video"
android:src="@drawable/play_circle_filled"
android:visibility="gone"
tools:visibility="visible" />
</org.pixeldroid.app.postCreation.SquareLayout> </org.pixeldroid.app.postCreation.SquareLayout>

View File

@ -1,9 +1,28 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ImageView xmlns:android="http://schemas.android.com/apk/res/android"
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/img"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent">
android:scaleType="centerInside"
tools:ignore="ContentDescription"
tools:src="@tools:sample/backgrounds/scenic" /> <ImageView
android:id="@+id/img"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="center"
android:adjustViewBounds="true"
tools:ignore="ContentDescription" />
<ImageButton
android:id="@+id/videoIndicator"
android:visibility="gone"
android:background="?attr/selectableItemBackgroundBorderless"
android:layout_width="48dp"
android:layout_height="48dp"
android:layout_gravity="center"
android:scaleType="fitCenter"
android:src="@drawable/play_circle_filled"
android:contentDescription="@string/play_video" />
</FrameLayout>

View File

@ -64,6 +64,13 @@
<string name="poll_notification_channel">"Polls"</string> <string name="poll_notification_channel">"Polls"</string>
<string name="other_notification_channel">"Other"</string> <string name="other_notification_channel">"Other"</string>
<plurals name="notification_title_summary">
<item quantity="one">"%d new notification"</item>
<item quantity="other">"%d new notifications"</item>
</plurals>
<string name="notification_summary_large">%1$s, %2$s, %3$s and %4$d others</string>
<string name="notification_summary_medium">%1$s, %2$s, and %3$s</string>
<string name="notification_summary_small">%1$s and %2$s</string>
<!-- Login page --> <!-- Login page -->
@ -107,6 +114,7 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="no_media_description">Add a media description here…</string> <string name="no_media_description">Add a media description here…</string>
<string name="total_exceeds_album_limit">You chose more images than the maximum your server allows (%1$s). Images beyond the limit have been ignored.</string> <string name="total_exceeds_album_limit">You chose more images than the maximum your server allows (%1$s). Images beyond the limit have been ignored.</string>
<string name="size_exceeds_instance_limit">Size of image number %1$d in the album exceeds the maximum size allowed by the instance (%2$d kB but the limit is %3$d kB). You might not be able to upload it.</string> <string name="size_exceeds_instance_limit">Size of image number %1$d in the album exceeds the maximum size allowed by the instance (%2$d kB but the limit is %3$d kB). You might not be able to upload it.</string>
<string name="video_not_supported">"The server you are using doesn't support video uploads, you might not be able to upload videos included in this post"</string>
<string name="upload_error">Error code returned by server: %1$d</string> <string name="upload_error">Error code returned by server: %1$d</string>
@ -165,6 +173,8 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="add_comment">Add a comment</string> <string name="add_comment">Add a comment</string>
<string name="submit_comment">Submit comment</string> <string name="submit_comment">Submit comment</string>
<string name="post_is_album">This post is an album</string> <string name="post_is_album">This post is an album</string>
<string name="post_is_video">This post is a video</string>
<!-- Profile page --> <!-- Profile page -->
<plurals name="nb_posts"> <plurals name="nb_posts">
<item quantity="one">"%d\nPost"</item> <item quantity="one">"%d\nPost"</item>
@ -250,4 +260,6 @@ For more info about Pixelfed, you can check here: https://pixelfed.org"</string>
<string name="login_notifications">Couldn\'t fetch latest notifications</string> <string name="login_notifications">Couldn\'t fetch latest notifications</string>
<string name="no_camera_permission">Camera permission not granted, grant the permission in settings if you want to let PixelDroid use the camera</string> <string name="no_camera_permission">Camera permission not granted, grant the permission in settings if you want to let PixelDroid use the camera</string>
<string name="no_storage_permission">Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail</string> <string name="no_storage_permission">Storage permission not granted, grant the permission in settings if you want to let PixelDroid show the thumbnail</string>
<string name="play_video">Play video</string>
<string name="video_edit_not_yet_supported">Video editing is not yet supported</string>
</resources> </resources>

View File

@ -0,0 +1,2 @@
* Aktualisierung von Übersetzungen
* Auswählen von nicht lokal gespeicherten Dateien (z.B. Nextcloud) beim Upload eingeführt

View File

@ -0,0 +1,4 @@
* Add support for video playback and upload
* Notification improvements
* Translation updates
* Update dependencies