Start implementing shareable post screenshot.

This commit is contained in:
Docile-Alligator 2025-06-09 17:23:55 -04:00
parent 5d8a66b272
commit 4296a55c18
7 changed files with 389 additions and 5 deletions

View File

@ -1,9 +1,10 @@
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
}
android {
compileSdk 34
compileSdk 35
defaultConfig {
applicationId "ml.docilealligator.infinityforreddit"
minSdk 21
@ -54,6 +55,9 @@ android {
viewBinding = true
}
namespace 'ml.docilealligator.infinityforreddit'
kotlinOptions {
jvmTarget = '11'
}
}
dependencies {
@ -65,6 +69,7 @@ dependencies {
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.legacy:legacy-support-v4:1.0.0'
implementation 'androidx.activity:activity:1.9.0'
implementation 'androidx.core:core-ktx:1.16.0'
def lifecycleVersion = "2.7.0"
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion"
implementation "androidx.lifecycle:lifecycle-livedata:$lifecycleVersion"
@ -193,6 +198,8 @@ dependencies {
implementation 'com.giphy.sdk:ui:2.3.15'
implementation 'com.github.alexzhirkevich:custom-qr-generator:2.0.0-alpha01'
/**** Builds and flavors ****/
// debugImplementation because LeakCanary should only run in debug builds.

View File

@ -120,6 +120,7 @@ import ml.docilealligator.infinityforreddit.thing.SaveThing;
import ml.docilealligator.infinityforreddit.thing.StreamableVideo;
import ml.docilealligator.infinityforreddit.thing.VoteThing;
import ml.docilealligator.infinityforreddit.utils.APIUtils;
import ml.docilealligator.infinityforreddit.utils.ShareScreenshotUtilsKt;
import ml.docilealligator.infinityforreddit.utils.SharedPreferencesUtils;
import ml.docilealligator.infinityforreddit.utils.Utils;
import ml.docilealligator.infinityforreddit.videoautoplay.CacheManager;
@ -181,6 +182,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
private RequestManager mGlide;
private int mMaxResolution;
private SaveMemoryCenterInisdeDownsampleStrategy mSaveMemoryCenterInsideDownsampleStrategy;
private CustomThemeWrapper mCustomThemeWrapper;
private Locale mLocale;
private boolean canStartActivity = true;
private int mPostType;
@ -382,6 +384,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
mGlide = Glide.with(mActivity);
mMaxResolution = Integer.parseInt(mSharedPreferences.getString(SharedPreferencesUtils.POST_FEED_MAX_RESOLUTION, "5000000"));
mSaveMemoryCenterInsideDownsampleStrategy = new SaveMemoryCenterInisdeDownsampleStrategy(mMaxResolution);
mCustomThemeWrapper = customThemeWrapper;
mLocale = locale;
mExoCreator = exoCreator;
mCallback = callback;
@ -2937,7 +2940,7 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
return false;
}
PostOptionsBottomSheetFragment postOptionsBottomSheetFragment;
/*PostOptionsBottomSheetFragment postOptionsBottomSheetFragment;
if (post.getPostType() == Post.GALLERY_TYPE && this instanceof PostBaseGalleryTypeViewHolder) {
postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post,
getBindingAdapterPosition(),
@ -2945,7 +2948,9 @@ public class PostRecyclerViewAdapter extends PagingDataAdapter<Post, RecyclerVie
} else {
postOptionsBottomSheetFragment = PostOptionsBottomSheetFragment.newInstance(post, getBindingAdapterPosition());
}
postOptionsBottomSheetFragment.show(mActivity.getSupportFragmentManager(), postOptionsBottomSheetFragment.getTag());
postOptionsBottomSheetFragment.show(mActivity.getSupportFragmentManager(), postOptionsBottomSheetFragment.getTag());*/
ShareScreenshotUtilsKt.sharePostAsScreenshot(mActivity, post, mCustomThemeWrapper, mLocale, mTimeFormatPattern, mSaveMemoryCenterInsideDownsampleStrategy);
return true;
});
}

View File

@ -303,6 +303,10 @@ public class ParsePost {
} else {
if (isVideo) {
//No preview video post
/*
TODO a removed crosspost may not have media JSONObject. This happens in crosspost_parent_list
e.g. https://www.reddit.com/r/hitmanimals/comments/1l6pv0m/mission_failed_agent_47/
*/
JSONObject redditVideoObject = data.getJSONObject(JSONUtils.MEDIA_KEY).getJSONObject(JSONUtils.REDDIT_VIDEO_KEY);
int postType = Post.VIDEO_TYPE;
String videoUrl = Html.fromHtml(redditVideoObject.getString(JSONUtils.HLS_URL_KEY)).toString();

View File

@ -0,0 +1,244 @@
package ml.docilealligator.infinityforreddit.utils
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.res.ColorStateList
import android.graphics.Bitmap
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.PorterDuff
import android.graphics.drawable.Drawable
import android.os.Handler
import android.os.Looper
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.widget.ImageView
import androidx.core.content.ContextCompat
import androidx.core.content.FileProvider
import com.bumptech.glide.Glide
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.MultiTransformation
import com.bumptech.glide.load.engine.GlideException
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestListener
import com.bumptech.glide.request.RequestOptions
import com.bumptech.glide.request.target.Target
import com.github.alexzhirkevich.customqrgenerator.QrData
import com.github.alexzhirkevich.customqrgenerator.vector.QrCodeDrawable
import com.github.alexzhirkevich.customqrgenerator.vector.QrVectorOptions
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorBallShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorColor
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorColors
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorFrameShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogo
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoPadding
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorLogoShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorPixelShape
import com.github.alexzhirkevich.customqrgenerator.vector.style.QrVectorShapes
import jp.wasabeef.glide.transformations.BlurTransformation
import ml.docilealligator.infinityforreddit.R
import ml.docilealligator.infinityforreddit.SaveMemoryCenterInisdeDownsampleStrategy
import ml.docilealligator.infinityforreddit.activities.BaseActivity
import ml.docilealligator.infinityforreddit.customtheme.CustomThemeWrapper
import ml.docilealligator.infinityforreddit.databinding.SharedPostBinding
import ml.docilealligator.infinityforreddit.post.Post
import java.io.File
import java.io.FileOutputStream
import java.io.IOException
import java.util.Locale
fun sharePostAsScreenshot(
baseActivity: BaseActivity, post: Post, customThemeWrapper: CustomThemeWrapper,
locale: Locale, timeFormatPattern: String,
saveMemoryCenterInsideDownsampleStrategy: SaveMemoryCenterInisdeDownsampleStrategy
) {
val binding: SharedPostBinding = SharedPostBinding.inflate(LayoutInflater.from(ContextThemeWrapper(baseActivity, R.style.AppTheme)))
binding.titleTextViewSharedPost.text = post.title
binding.subredditNameTextViewSharedPost.text = post.subredditNamePrefixed
binding.userTextViewSharedPost.text = post.authorNamePrefixed
binding.postTimeTextViewSharedPost.text = Utils.getFormattedTime(
locale,
post.postTimeMillis,
timeFormatPattern
)
binding.scoreTextViewSharedPost.text = post.score.toString()
binding.commentsCountTextViewSharedPost.text = post.nComments.toString()
binding.root.setBackgroundTintList(ColorStateList.valueOf(customThemeWrapper.filledCardViewBackgroundColor))
binding.titleTextViewSharedPost.setTextColor(customThemeWrapper.postTitleColor)
binding.contentTextViewSharedPost.setTextColor(customThemeWrapper.postContentColor)
binding.subredditNameTextViewSharedPost.setTextColor(customThemeWrapper.subreddit)
binding.userTextViewSharedPost.setTextColor(customThemeWrapper.username)
binding.postTimeTextViewSharedPost.setTextColor(customThemeWrapper.secondaryTextColor)
binding.scoreTextViewSharedPost.setTextColor(customThemeWrapper.upvoted)
binding.commentsCountTextViewSharedPost.setTextColor(customThemeWrapper.postIconAndInfoColor)
binding.upvoteImageViewSharedPost.setColorFilter(
customThemeWrapper.upvoted,
PorterDuff.Mode.SRC_IN
)
binding.commentImageViewSharedPost.setColorFilter(
customThemeWrapper.postIconAndInfoColor,
PorterDuff.Mode.SRC_IN
)
binding.titleTextViewSharedPost.setTypeface(baseActivity.titleTypeface)
binding.contentTextViewSharedPost.setTypeface(baseActivity.contentTypeface)
binding.subredditNameTextViewSharedPost.setTypeface(baseActivity.titleTypeface)
binding.userTextViewSharedPost.setTypeface(baseActivity.titleTypeface)
binding.postTimeTextViewSharedPost.setTypeface(baseActivity.titleTypeface)
binding.scoreTextViewSharedPost.setTypeface(baseActivity.titleTypeface)
binding.commentsCountTextViewSharedPost.setTypeface(baseActivity.titleTypeface)
val data: QrData.Url = QrData.Url(post.permalink)
val qrCode: Drawable = QrCodeDrawable(
data, QrVectorOptions.Builder()
.setLogo(
QrVectorLogo(
drawable = ContextCompat.getDrawable(baseActivity, R.mipmap.ic_launcher_round),
size = .3f,
padding = QrVectorLogoPadding.Natural(.1f),
shape = QrVectorLogoShape.Circle
)
)
.setColors(
QrVectorColors(
dark = QrVectorColor.Solid(customThemeWrapper.colorAccent),
ball = QrVectorColor.Solid(customThemeWrapper.colorAccent),
frame = QrVectorColor.Solid(customThemeWrapper.colorAccent)
)
)
.setShapes(
QrVectorShapes(
darkPixel = QrVectorPixelShape.RoundCorners(0.5f),
ball = QrVectorBallShape.RoundCorners(0.5f),
frame = QrVectorFrameShape.RoundCorners(0.25f),
)
).build()
)
binding.qrCodeImageViewSharedPost.setImageDrawable(qrCode)
when (post.postType) {
Post.VIDEO_TYPE, Post.GIF_TYPE, Post.IMAGE_TYPE, Post.GALLERY_TYPE, Post.LINK_TYPE -> {
binding.contentTextViewSharedPost.visibility = View.GONE
val preview = if (post.previews.isNotEmpty()) post.previews[0] else null
if (preview != null) {
val height = (400 * baseActivity.resources.displayMetrics.density).toInt()
binding.imageViewSharedPost.setScaleType(ImageView.ScaleType.CENTER_CROP)
binding.imageViewSharedPost.layoutParams.height = height
measureView(binding.getRoot())
val blurImage = post.isNSFW || post.isSpoiler
val url = preview.previewUrl
val imageRequestBuilder = Glide.with(baseActivity).load(url)
.listener(object : RequestListener<Drawable> {
override fun onLoadFailed(
e: GlideException?,
model: Any?,
target: Target<Drawable>,
isFirstResource: Boolean
): Boolean {
binding.imageViewSharedPost.setVisibility(View.GONE)
measureView(binding.getRoot())
shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot()))
return false
}
override fun onResourceReady(
resource: Drawable,
model: Any,
target: Target<Drawable>,
dataSource: DataSource,
isFirstResource: Boolean
): Boolean {
Handler(Looper.getMainLooper()).post {
shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot()))
}
return false
}
})
if (blurImage) {
imageRequestBuilder.apply(
RequestOptions.bitmapTransform(
MultiTransformation(
BlurTransformation(50, 10),
RoundedCorners(16)
)
)
)
.into(binding.imageViewSharedPost)
} else {
imageRequestBuilder.centerInside().apply(RequestOptions().transform(
RoundedCorners(50)
)).downsample(
saveMemoryCenterInsideDownsampleStrategy
).into(binding.imageViewSharedPost)
}
} else {
}
return
}
Post.NO_PREVIEW_LINK_TYPE -> {
binding.contentTextViewSharedPost.text = post.url
binding.imageViewSharedPost.setVisibility(View.GONE)
}
else -> {
binding.contentTextViewSharedPost.text = post.selfTextPlainTrimmed
binding.imageViewSharedPost.setVisibility(View.GONE)
}
}
measureView(binding.getRoot())
shareScreenshot(baseActivity, getBitmapFromView(binding.getRoot()))
}
private fun measureView(rootView: View) {
val specWidth = View.MeasureSpec.makeMeasureSpec(1920, View.MeasureSpec.EXACTLY)
val specHeight = View.MeasureSpec.makeMeasureSpec(0, View.MeasureSpec.UNSPECIFIED)
rootView.measure(specWidth, specHeight)
rootView.layout(0, 0, rootView.measuredWidth, rootView.measuredHeight)
}
private fun getBitmapFromView(rootView: View): Bitmap {
val bitmap = Bitmap.createBitmap(rootView.width, rootView.height, Bitmap.Config.ARGB_8888)
val canvas = Canvas(bitmap)
val bgDrawable = rootView.background
if (bgDrawable != null) bgDrawable.draw(canvas)
else canvas.drawColor(Color.WHITE)
rootView.draw(canvas)
return bitmap
}
private fun shareScreenshot(context: Context, bitmap: Bitmap) {
try {
val cachePath = File(context.externalCacheDir, "images")
if (!cachePath.exists()) {
cachePath.mkdirs()
}
val file = File(cachePath, "shared_post.png")
val stream = FileOutputStream(file)
bitmap.compress(Bitmap.CompressFormat.PNG, 100, stream)
stream.close()
val uri = FileProvider.getUriForFile(
context,
context.packageName + ".provider",
file
)
val intent = Intent(Intent.ACTION_SEND)
intent.setType("image/png")
intent.putExtra(Intent.EXTRA_STREAM, uri)
intent.clipData = ClipData.newRawUri("", uri)
intent.setFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
context.startActivity(Intent.createChooser(intent, "Share"))
} catch (e: IOException) {
e.printStackTrace()
}
}

View File

@ -0,0 +1,120 @@
<?xml version="1.0" encoding="utf-8"?>
<com.google.android.material.card.MaterialCardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
app:cardCornerRadius="24dp"
style="?attr/materialCardViewElevatedStyle">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="32dp">
<TextView
android:id="@+id/title_text_view_shared_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:textSize="36sp" />
<TextView
android:id="@+id/content_text_view_shared_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:ellipsize="end"
android:maxLines="4"
android:textSize="20sp" />
<ImageView
android:id="@+id/image_view_shared_post"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:adjustViewBounds="true" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:layout_gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/subreddit_name_text_view_shared_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textSize="?attr/font_default" />
<TextView
android:id="@+id/user_text_view_shared_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="8dp"
android:textSize="?attr/font_default" />
<TextView
android:id="@+id/post_time_text_view_shared_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:textSize="?attr/font_default" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="16dp">
<ImageView
android:id="@+id/upvote_image_view_shared_post"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="center_vertical"
android:src="@drawable/ic_upvote_filled_24dp" />
<TextView
android:id="@+id/score_text_view_shared_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:textSize="?attr/font_default" />
<ImageView
android:id="@+id/comment_image_view_shared_post"
android:layout_width="16dp"
android:layout_height="16dp"
android:layout_gravity="bottom"
android:layout_marginStart="16dp"
android:src="@drawable/ic_comment_grey_24dp" />
<TextView
android:id="@+id/comments_count_text_view_shared_post"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dp"
android:textSize="?attr/font_default" />
</LinearLayout>
</LinearLayout>
<ImageView
android:id="@+id/qr_code_image_view_shared_post"
android:layout_width="96dp"
android:layout_height="96dp"
android:layout_gravity="center_vertical" />
</LinearLayout>
</LinearLayout>
</com.google.android.material.card.MaterialCardView>

View File

@ -1,4 +1,7 @@
buildscript {
ext {
kotlin_version = '1.9.24'
}
repositories {
google()
mavenCentral()
@ -6,7 +9,8 @@ buildscript {
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:8.5.0'
classpath 'com.android.tools.build:gradle:8.8.0'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
// NOTE: Do not place your application dependencies here; they belong
// in the individual module build.gradle files

View File

@ -1,5 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists