Add new APNG renderer, just for internal users for now.

This commit is contained in:
Greyson Parrelli
2024-02-02 10:08:08 -05:00
committed by Cody Henthorne
parent 34d87cf6e1
commit c3f9e5d972
151 changed files with 2425 additions and 13 deletions

View File

@@ -613,6 +613,7 @@ dependencies {
implementation(project(":core:models-jvm"))
implementation(project(":feature:camera"))
implementation(project(":feature:registration"))
implementation(project(":lib:apng"))
implementation(libs.androidx.fragment.ktx)
implementation(libs.androidx.appcompat) {

View File

@@ -12,4 +12,5 @@ sealed interface LabsSettingsEvents {
data class ToggleGroupSuggestionsForMembers(val enabled: Boolean) : LabsSettingsEvents
data class ToggleBetterSearch(val enabled: Boolean) : LabsSettingsEvents
data class ToggleAutoLowerHand(val enabled: Boolean) : LabsSettingsEvents
data class ToggleNewApngRenderer(val enabled: Boolean) : LabsSettingsEvents
}

View File

@@ -142,6 +142,15 @@ private fun LabsSettingsContent(
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleAutoLowerHand(it)) }
)
}
item {
Rows.ToggleRow(
checked = state.newApngRenderer,
text = "New APNG Renderer",
label = "Use the new custom APNG renderer instead of the existing third-party library. Requires an app restart to take effect.",
onCheckChanged = { onEvent(LabsSettingsEvents.ToggleNewApngRenderer(it)) }
)
}
}
}
}

View File

@@ -14,5 +14,6 @@ data class LabsSettingsState(
val incognito: Boolean = false,
val groupSuggestionsForMembers: Boolean = false,
val betterSearch: Boolean = false,
val autoLowerHand: Boolean = false
val autoLowerHand: Boolean = false,
val newApngRenderer: Boolean = false
)

View File

@@ -41,6 +41,10 @@ class LabsSettingsViewModel : ViewModel() {
SignalStore.labs.autoLowerHand = event.enabled
_state.value = _state.value.copy(autoLowerHand = event.enabled)
}
is LabsSettingsEvents.ToggleNewApngRenderer -> {
SignalStore.labs.newApngRenderer = event.enabled
_state.value = _state.value.copy(newApngRenderer = event.enabled)
}
}
}
@@ -51,7 +55,8 @@ class LabsSettingsViewModel : ViewModel() {
incognito = SignalStore.labs.incognito,
groupSuggestionsForMembers = SignalStore.labs.groupSuggestionsForMembers,
betterSearch = SignalStore.labs.betterSearch,
autoLowerHand = SignalStore.labs.autoLowerHand
autoLowerHand = SignalStore.labs.autoLowerHand,
newApngRenderer = SignalStore.labs.newApngRenderer
)
}
}

View File

@@ -16,11 +16,12 @@ import com.bumptech.glide.load.resource.gif.ByteBufferGifDecoder;
import com.bumptech.glide.load.resource.gif.GifDrawable;
import com.bumptech.glide.load.resource.gif.StreamGifDecoder;
import org.signal.apng.ApngDecoder;
import org.signal.blurhash.BlurHash;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.signal.glide.blurhash.BlurHashModelLoader;
import org.signal.glide.blurhash.BlurHashResourceDecoder;
import org.signal.glide.common.io.InputStreamFactory;
import org.signal.glide.load.resource.apng.decode.APNGDecoder;
import org.thoughtcrime.securesms.badges.load.BadgeLoader;
import org.thoughtcrime.securesms.badges.load.GiftBadgeModel;
import org.thoughtcrime.securesms.badges.models.Badge;
@@ -29,9 +30,13 @@ import org.thoughtcrime.securesms.contacts.avatars.ContactPhotoLoader;
import org.thoughtcrime.securesms.crypto.AttachmentSecret;
import org.thoughtcrime.securesms.crypto.AttachmentSecretProvider;
import org.thoughtcrime.securesms.giph.model.ChunkedImageUrl;
import org.thoughtcrime.securesms.glide.cache.ApngDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ApngFrameDrawableTranscoder;
import org.thoughtcrime.securesms.glide.cache.ApngInputStreamFactoryResourceDecoder;
import org.thoughtcrime.securesms.glide.cache.ApngInputStreamResourceDecoder;
import org.thoughtcrime.securesms.glide.cache.ByteBufferApngDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngCacheEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedApngResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedBitmapResourceEncoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheDecoder;
import org.thoughtcrime.securesms.glide.cache.EncryptedCacheEncoder;
@@ -46,6 +51,7 @@ import org.signal.glide.decryptableuri.DecryptableUri;
import org.signal.glide.decryptableuri.DecryptableUriStreamLoader;
import org.thoughtcrime.securesms.mms.RegisterGlideComponents;
import org.thoughtcrime.securesms.mms.SignalGlideModule;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
import org.thoughtcrime.securesms.stickers.StickerRemoteUri;
import org.thoughtcrime.securesms.stickers.StickerRemoteUriLoader;
import org.thoughtcrime.securesms.stories.StoryTextPostModel;
@@ -85,16 +91,25 @@ public class SignalGlideComponents implements RegisterGlideComponents {
registry.prepend(Bitmap.class, new EncryptedBitmapResourceEncoder(secret));
registry.prepend(BitmapDrawable.class, new BitmapDrawableEncoder(glide.getBitmapPool(), encryptedBitmapResourceEncoder));
ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder();
StreamApngDecoder streamApngDecoder = new StreamApngDecoder(byteBufferApngDecoder);
StreamFactoryApngDecoder streamFactoryApngDecoder = new StreamFactoryApngDecoder(byteBufferApngDecoder, glide, registry);
registry.prepend(InputStream.class, APNGDecoder.class, streamApngDecoder);
registry.prepend(InputStreamFactory.class, APNGDecoder.class, streamFactoryApngDecoder);
registry.prepend(ByteBuffer.class, APNGDecoder.class, byteBufferApngDecoder);
registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret));
registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, streamApngDecoder));
registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder());
if (SignalStore.labs().getNewApngRenderer()) {
registry.prepend(InputStream.class, ApngDecoder.class, new ApngInputStreamResourceDecoder());
registry.prepend(InputStreamFactory.class, ApngDecoder.class, new ApngInputStreamFactoryResourceDecoder());
registry.prepend(ApngDecoder.class, new EncryptedApngResourceEncoder(secret));
registry.prepend(File.class, ApngDecoder.class, new EncryptedCacheDecoder<>(secret, new ApngInputStreamResourceDecoder()));
registry.register(ApngDecoder.class, Drawable.class, new ApngDrawableTranscoder());
} else {
ByteBufferApngDecoder byteBufferApngDecoder = new ByteBufferApngDecoder();
StreamApngDecoder streamApngDecoder = new StreamApngDecoder(byteBufferApngDecoder);
StreamFactoryApngDecoder streamFactoryApngDecoder = new StreamFactoryApngDecoder(byteBufferApngDecoder, glide, registry);
registry.prepend(InputStream.class, APNGDecoder.class, streamApngDecoder);
registry.prepend(InputStreamFactory.class, APNGDecoder.class, streamFactoryApngDecoder);
registry.prepend(ByteBuffer.class, APNGDecoder.class, byteBufferApngDecoder);
registry.prepend(APNGDecoder.class, new EncryptedApngCacheEncoder(secret));
registry.prepend(File.class, APNGDecoder.class, new EncryptedCacheDecoder<>(secret, streamApngDecoder));
registry.register(APNGDecoder.class, Drawable.class, new ApngFrameDrawableTranscoder());
}
registry.prepend(BlurHash.class, Bitmap.class, new BlurHashResourceDecoder());
registry.prepend(StoryTextPostModel.class, Bitmap.class, new StoryTextPostModel.Decoder());

View File

@@ -0,0 +1,28 @@
package org.thoughtcrime.securesms.glide.cache
import android.graphics.drawable.Drawable
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.engine.Resource
import com.bumptech.glide.load.resource.drawable.DrawableResource
import com.bumptech.glide.load.resource.transcode.ResourceTranscoder
import org.signal.apng.ApngDecoder
import org.signal.apng.ApngDrawable
class ApngDrawableTranscoder : ResourceTranscoder<ApngDecoder, Drawable> {
override fun transcode(toTranscode: Resource<ApngDecoder>, options: Options): Resource<Drawable> {
val decoder = toTranscode.get()
val drawable = ApngDrawable(decoder).apply {
loopForever = true
}
return object : DrawableResource<Drawable>(drawable) {
override fun getResourceClass(): Class<Drawable> = Drawable::class.java
override fun getSize(): Int = 0
override fun recycle() {
(get() as ApngDrawable).recycle()
}
}
}
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
import org.signal.core.util.readFully
import org.signal.glide.apng.ApngOptions
import org.signal.glide.common.io.InputStreamFactory
import java.io.ByteArrayInputStream
import java.io.IOException
class ApngInputStreamFactoryResourceDecoder : ResourceDecoder<InputStreamFactory, ApngDecoder> {
override fun handles(source: InputStreamFactory, options: Options): Boolean {
return if (options.get(ApngOptions.ANIMATE) == true) {
ApngDecoder.isApng(source.create())
} else {
false
}
}
@Throws(IOException::class)
override fun decode(source: InputStreamFactory, width: Int, height: Int, options: Options): Resource<ApngDecoder>? {
val data: ByteArray = source.create().readFully()
val decoder = ApngDecoder(ByteArrayInputStream(data))
return ApngResource(decoder, data.size)
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceDecoder
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
import org.signal.core.util.readFully
import org.signal.core.util.stream.LimitedInputStream
import org.signal.glide.apng.ApngOptions
import java.io.ByteArrayInputStream
import java.io.IOException
import java.io.InputStream
class ApngInputStreamResourceDecoder : ResourceDecoder<InputStream, ApngDecoder> {
companion object {
/** Set to match [com.bumptech.glide.load.data.InputStreamRewinder]'s read limit */
private const val READ_LIMIT: Long = 5 * 1024 * 1024
}
override fun handles(source: InputStream, options: Options): Boolean {
return if (options.get(ApngOptions.ANIMATE)!!) {
ApngDecoder.isApng(LimitedInputStream(source, READ_LIMIT))
} else {
false
}
}
@Throws(IOException::class)
override fun decode(source: InputStream, width: Int, height: Int, options: Options): Resource<ApngDecoder>? {
val data: ByteArray = source.readFully()
val decoder = ApngDecoder(ByteArrayInputStream(data))
return ApngResource(decoder, data.size)
}
}

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
class ApngResource(private val decoder: ApngDecoder, private val size: Int) : Resource<ApngDecoder> {
override fun getResourceClass(): Class<ApngDecoder> = ApngDecoder::class.java
override fun get(): ApngDecoder = decoder
override fun getSize(): Int = size
override fun recycle() {
decoder.inputStream.close()
}
}

View File

@@ -0,0 +1,36 @@
package org.thoughtcrime.securesms.glide.cache
import com.bumptech.glide.load.EncodeStrategy
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.ResourceEncoder
import com.bumptech.glide.load.engine.Resource
import org.signal.apng.ApngDecoder
import org.signal.core.util.logging.Log.tag
import org.signal.core.util.logging.Log.w
import java.io.File
import java.io.IOException
internal class EncryptedApngResourceEncoder(private val secret: ByteArray) : EncryptedCoder(), ResourceEncoder<ApngDecoder> {
override fun getEncodeStrategy(options: Options): EncodeStrategy {
return EncodeStrategy.SOURCE
}
override fun encode(data: Resource<ApngDecoder>, file: File, options: Options): Boolean {
try {
val input = data.get().inputStream
val output = createEncryptedOutputStream(secret, file)
input.reset()
input.copyTo(output)
return true
} catch (e: IOException) {
w(TAG, e)
}
return false
}
companion object {
private val TAG = tag(EncryptedApngResourceEncoder::class.java)
}
}

View File

@@ -10,6 +10,7 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
const val GROUP_SUGGESTIONS_FOR_MEMBERS: String = "labs.group_suggestions_for_members"
const val BETTER_SEARCH: String = "labs.better_search"
const val AUTO_LOWER_HAND: String = "labs.auto_lower_hand"
const val NEW_APNG_RENDERER: String = "labs.new_apng_renderer"
}
public override fun onFirstEverAppLaunch() = Unit
@@ -28,6 +29,8 @@ class LabsValues internal constructor(store: KeyValueStore) : SignalStoreValues(
var autoLowerHand by booleanValue(AUTO_LOWER_HAND, true).falseForExternalUsers()
var newApngRenderer by booleanValue(NEW_APNG_RENDERER, true).falseForExternalUsers()
private fun SignalStoreValueDelegate<Boolean>.falseForExternalUsers(): SignalStoreValueDelegate<Boolean> {
return this.map { actualValue -> RemoteConfig.internalUser && actualValue }
}

View File

@@ -12,6 +12,7 @@ import androidx.annotation.RequiresApi;
import org.signal.core.util.logging.Log;
import org.thoughtcrime.securesms.dependencies.AppDependencies;
import org.thoughtcrime.securesms.keyvalue.SignalStore;
/**
* Easy access to various properties of the device, typically to make performance-related decisions.
@@ -25,6 +26,10 @@ public final class DeviceProperties {
* large numbers of APNGs simultaneously.
*/
public static boolean shouldAllowApngStickerAnimation(@NonNull Context context) {
if (SignalStore.labs().getNewApngRenderer()) {
return true;
}
MemoryInfo memoryInfo = getMemoryInfo(context);
int memoryMb = (int) ByteUnit.BYTES.toMegabytes(memoryInfo.totalMem);

View File

@@ -0,0 +1,17 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util
import java.nio.ByteBuffer
import java.nio.ByteOrder
fun ByteArray.toUInt(): UInt {
return ByteBuffer.wrap(this).order(ByteOrder.BIG_ENDIAN).int.toUInt()
}
fun ByteArray.toUShort(): UShort {
return ByteBuffer.wrap(this).order(ByteOrder.BIG_ENDIAN).getShort().toUShort()
}

View File

@@ -10,6 +10,7 @@ import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.InputStream
import java.io.OutputStream
import kotlin.jvm.Throws
import kotlin.math.min
/**
@@ -107,6 +108,13 @@ fun InputStream.readLength(): Long {
return count
}
/**
* Reads a 32-bit unsigned integer from the stream.
*/
fun InputStream.readUInt(): UInt {
return this.readNBytesOrThrow(4).toUInt()
}
/**
* Reads the contents of the stream and discards them.
*/

View File

@@ -6,6 +6,8 @@
package org.signal.core.util
import java.io.OutputStream
import java.nio.ByteBuffer
import java.nio.ByteOrder
/**
* Writes a 32-bit variable-length integer to the stream.
@@ -30,3 +32,11 @@ fun OutputStream.writeVarInt32(value: Int) {
}
}
}
/**
* Writes a 32-bit unsigned integer to the stream.
*/
fun OutputStream.writeUInt(value: UInt) {
// Note that casting to an int here is fine, because at the end of the day, we're just writing 4 bytes to the stream
this.write(ByteBuffer.allocate(4).order(ByteOrder.BIG_ENDIAN).putInt(value.toInt()).array())
}

View File

@@ -0,0 +1,34 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.core.util.stream
import java.io.FilterOutputStream
import java.io.OutputStream
import java.util.zip.CRC32
/**
* A simple pass-through stream that calculates a CRC32 as data is written to the target [OutputStream].
*/
class Crc32OutputStream(private val wrapped: OutputStream) : FilterOutputStream(wrapped) {
private val crc32 = CRC32()
val currentCrc32: Long
get() = crc32.value
override fun write(byte: Int) {
wrapped.write(byte)
crc32.update(byte)
}
override fun write(data: ByteArray) {
write(data, 0, data.size)
}
override fun write(data: ByteArray, offset: Int, length: Int) {
wrapped.write(data, offset, length)
crc32.update(data, offset, length)
}
}

1
demo/apng/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

View File

@@ -0,0 +1,16 @@
plugins {
id("signal-sample-app")
alias(libs.plugins.compose.compiler)
}
android {
namespace = "org.signal.apng"
defaultConfig {
applicationId = "org.signal.apng"
}
}
dependencies {
implementation(project(":lib:apng"))
}

21
demo/apng/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

View File

@@ -0,0 +1,39 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.Signal">
<activity
android:name=".MainActivity"
android:exported="true"
android:label="@string/app_name"
android:theme="@style/Theme.Signal">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".DemoActivity"
android:exported="false"
android:theme="@style/Theme.Signal" />
<activity
android:name=".PlayerActivity"
android:exported="false"
android:theme="@style/Theme.Signal" />
</application>
</manifest>

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 91 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 221 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 296 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 370 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 249 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 307 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 470 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 261 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 273 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 486 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 617 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 572 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 780 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 371 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 613 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 492 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 671 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 534 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 614 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 579 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 508 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 646 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 915 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 331 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 668 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 308 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 276 B

View File

@@ -0,0 +1,298 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.apng
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.ComponentActivity
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.ListAdapter
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.ViewHolder
class DemoActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.demo_activity)
val myAdapter = MyAdapter()
findViewById<RecyclerView?>(R.id.list).apply {
layoutManager = LinearLayoutManager(this@DemoActivity)
adapter = myAdapter
}
// All test cases taken from:
// https://philip.html5.org/tests/apng/tests.html
myAdapter.submitList(
listOf(
TestModel.Heading("Basic cases"),
// TODO we don't yet render non-apngs
// TestModel(
// filename = "test00.png",
// description = "Trivial static image. This should be solid green."
// ),
TestModel.ApngModel(
filename = "test01.png",
description = "Trivial animated image - one frame; using default image. This should be solid green."
),
TestModel.ApngModel(
filename = "test02.png",
description = "Trivial animated image - one frame; ignoring default image. This should be solid green."
),
// IDAT, fdAT splitting
// TODO we don't yet support split IDAT/fdAT
// _root_ide_package_.org.signal.apng.MainActivity.TestModel.ApngModel(
// filename = "test03.png",
// description = "Basic split IDAT. This should be solid green."
// ),
// _root_ide_package_.org.signal.apng.MainActivity.TestModel.ApngModel(
// filename = "test04.png",
// description = "Split IDAT with zero-length chunk. This should be solid green."
// ),
// _root_ide_package_.org.signal.apng.MainActivity.TestModel.ApngModel(
// filename = "test05.png",
// description = "Basic split fdAT. This should be solid green."
// ),
// _root_ide_package_.org.signal.apng.MainActivity.TestModel.ApngModel(
// filename = "test06.png",
// description = "Split fdAT with zero-length chunk. This should be solid green."
// ),
TestModel.Heading("Dispose ops"),
TestModel.ApngModel(
filename = "test07.png",
description = "APNG_DISPOSE_OP_NONE - basic. This should be solid green."
),
TestModel.ApngModel(
filename = "test08.png",
description = "APNG_DISPOSE_OP_BACKGROUND - basic. This should be transparent."
),
TestModel.ApngModel(
filename = "test09.png",
description = "APNG_DISPOSE_OP_BACKGROUND - final frame. This should be solid green."
),
TestModel.ApngModel(
filename = "test10.png",
description = "APNG_DISPOSE_OP_PREVIOUS - basic. This should be solid green."
),
TestModel.ApngModel(
filename = "test11.png",
description = "APNG_DISPOSE_OP_PREVIOUS - final frame. This should be solid green."
),
TestModel.ApngModel(
filename = "test12.png",
description = "APNG_DISPOSE_OP_PREVIOUS - first frame. This should be transparent."
),
TestModel.Heading("Dispose ops and regions"),
TestModel.ApngModel(
filename = "test13.png",
description = "APNG_DISPOSE_OP_NONE in region. This should be solid green."
),
TestModel.ApngModel(
filename = "test14.png",
description = "APNG_DISPOSE_OP_BACKGROUND before region. This should be transparent."
),
TestModel.ApngModel(
filename = "test15.png",
description = "APNG_DISPOSE_OP_BACKGROUND in region. This should be a solid blue rectangle containing a smaller transparent rectangle."
),
TestModel.ApngModel(
filename = "test16.png",
description = "APNG_DISPOSE_OP_PREVIOUS in region. This should be solid green."
),
TestModel.Heading("Blend ops"),
TestModel.ApngModel(
filename = "test17.png",
description = "APNG_BLEND_OP_SOURCE on solid colour. This should be solid green."
),
TestModel.ApngModel(
filename = "test18.png",
description = "APNG_BLEND_OP_SOURCE on transparent colour. This should be transparent."
),
TestModel.ApngModel(
filename = "test19.png",
description = "APNG_BLEND_OP_SOURCE on nearly-transparent colour. This should be very nearly transparent."
),
TestModel.ApngModel(
filename = "test20.png",
description = "APNG_BLEND_OP_OVER on solid and transparent colours. This should be solid green. "
),
TestModel.ApngModel(
filename = "test21.png",
description = "APNG_BLEND_OP_OVER repeatedly with nearly-transparent colours. This should be solid green."
),
TestModel.Heading("Blending and gamma"),
TestModel.ApngModel(
filename = "test22.png",
description = "APNG_BLEND_OP_OVER This should be solid slightly-dark green."
),
TestModel.ApngModel(
filename = "test23.png",
description = "APNG_BLEND_OP_OVER This should be solid nearly-black."
),
TestModel.Heading("Chunk ordering"),
TestModel.ApngModel(
filename = "test24.png",
description = "fcTL before acTL. This should be solid green."
),
TestModel.Heading("Delays"),
TestModel.ApngModel(
filename = "test25.png",
description = "Basic delays. This should flash blue for half a second, then yellow for one second, then repeat."
),
TestModel.ApngModel(
filename = "test26.png",
description = "Rounding of division. This should flash blue for half a second, then yellow for one second, then repeat."
),
TestModel.ApngModel(
filename = "test27.png",
description = "16-bit numerator/denominator. This should flash blue for half a second, then yellow for one second, then repeat."
),
TestModel.ApngModel(
filename = "test28.png",
description = "Zero denominator. This should flash blue for half a second, then yellow for one second, then repeat."
),
TestModel.ApngModel(
filename = "test29.png",
description = "Zero numerator. This should flash cyan for a short period of time (perhaps zero), then magenta for the same short period of time, then blue for half a second, then yellow for one second, then repeat."
),
TestModel.Heading("num_plays"),
TestModel.ApngModel(
filename = "test30.png",
description = "num_plays = 0. This should flash yellow for one second, then blue for one second, then repeat forever."
),
TestModel.ApngModel(
filename = "test31.png",
description = "num_plays = 1. When first loaded, this should flash yellow for one second, then stay blue forever."
),
TestModel.ApngModel(
filename = "test32.png",
description = "num_plays = 2. When first loaded, this should flash yellow for one second, then blue for one second, then yellow for one second, then blue forever."
),
TestModel.Heading("Other depths and color types"),
TestModel.ApngModel(
filename = "test33.png",
description = "16-bit colour. This should be dark blue."
),
TestModel.ApngModel(
filename = "test34.png",
description = "8-bit greyscale. This should be a solid grey rectangle containing a solid white rectangle."
),
TestModel.ApngModel(
filename = "test35.png",
description = "8-bit greyscale and alpha, with blending. This should be solid grey."
),
TestModel.ApngModel(
filename = "test36.png",
description = "2-color palette. This should be solid green."
),
TestModel.ApngModel(
filename = "test37.png",
description = "2-bit palette and alpha. This should be solid green."
),
TestModel.ApngModel(
filename = "test38.png",
description = "1-bit palette and alpha, with blending. This should be solid dark blue."
),
TestModel.Heading("Random sample images"),
TestModel.ApngModel(
filename = "clock.png",
description = "A clock that uses BLEND_OP_OVER to draw moving hands."
),
TestModel.ApngModel(
filename = "ball.png",
description = "Classic bouncing ball."
),
TestModel.ApngModel(
filename = "elephant.png",
description = "A cute elephant"
)
)
)
}
sealed class TestModel {
data class Heading(val description: String) : TestModel()
data class ApngModel(val filename: String, val description: String) : TestModel()
}
private class MyAdapter : ListAdapter<TestModel, TestViewHolder>(object : DiffUtil.ItemCallback<TestModel>() {
override fun areItemsTheSame(oldItem: TestModel, newItem: TestModel): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: TestModel, newItem: TestModel): Boolean {
return oldItem == newItem
}
}) {
override fun getItemViewType(position: Int): Int {
return when (getItem(position)) {
is TestModel.Heading -> 1
is TestModel.ApngModel -> 2
}
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TestViewHolder {
when (viewType) {
1 -> return TestViewHolder.HeadingViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item_heading, parent, false))
2 -> return TestViewHolder.ApngViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.list_item_apng, parent, false))
else -> throw IllegalStateException()
}
}
override fun onBindViewHolder(holder: TestViewHolder, position: Int) {
holder.bind(getItem(position))
}
}
sealed class TestViewHolder(itemView: View) : ViewHolder(itemView) {
abstract fun bind(testModel: TestModel)
class HeadingViewHolder(itemView: View) : TestViewHolder(itemView) {
val description: TextView = itemView.findViewById(R.id.description)
override fun bind(testModel: TestModel) {
if (testModel !is TestModel.Heading) {
throw IllegalStateException()
}
description.text = testModel.description
}
}
class ApngViewHolder(itemView: View) : TestViewHolder(itemView) {
val description: TextView = itemView.findViewById(R.id.description)
val image: ImageView = itemView.findViewById(R.id.image)
override fun bind(testModel: TestModel) {
if (testModel !is TestModel.ApngModel) {
throw IllegalStateException()
}
description.text = testModel.description
val decoder = ApngDecoder(itemView.context.assets.open(testModel.filename))
val drawable = ApngDrawable(decoder)
image.setImageDrawable(drawable)
}
}
}
}

View File

@@ -0,0 +1,25 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.apng
import android.content.Intent
import android.os.Bundle
import android.widget.Button
import androidx.activity.ComponentActivity
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.main_activity)
findViewById<Button>(R.id.demo_button).setOnClickListener {
startActivity(Intent(this, DemoActivity::class.java))
}
findViewById<Button>(R.id.player_button).setOnClickListener {
startActivity(Intent(this, PlayerActivity::class.java))
}
}
}

View File

@@ -0,0 +1,67 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.apng
import android.annotation.SuppressLint
import android.os.Bundle
import android.widget.Button
import android.widget.ImageView
import android.widget.TextView
import androidx.activity.ComponentActivity
class PlayerActivity : ComponentActivity() {
lateinit var frameMetadata: TextView
lateinit var disposeOpText: TextView
lateinit var blendOpText: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.player_activity)
val image = findViewById<ImageView>(R.id.player_image)
val prevButton = findViewById<Button>(R.id.prev_button)
val nextButton = findViewById<Button>(R.id.next_button)
frameMetadata = findViewById<TextView>(R.id.frame_metadata)
val decoder = ApngDecoder(assets.open("broken03.png"))
val drawable = ApngDrawable(decoder).apply {
stop()
debugDrawBounds = true
}
image.setImageDrawable(drawable)
prevButton.setOnClickListener {
drawable.prevFrame()
updateFrameInfo(drawable)
}
nextButton.setOnClickListener {
drawable.nextFrame()
updateFrameInfo(drawable)
}
updateFrameInfo(drawable)
}
@SuppressLint("SetTextI18n")
private fun updateFrameInfo(drawable: ApngDrawable) {
frameMetadata.text = """
Frame: ${drawable.position + 1}/${drawable.frameCount}
--
Width: ${drawable.currentFrame.fcTL.width}
Height: ${drawable.currentFrame.fcTL.height}
--
xOffset: ${drawable.currentFrame.fcTL.xOffset}
yOffset: ${drawable.currentFrame.fcTL.yOffset}
--
DisposeOp: ${drawable.currentFrame.fcTL.disposeOp.name}
BlendOp: ${drawable.currentFrame.fcTL.blendOp.name}
WARNING: Going backwards can break rendering.
""".trimIndent()
}
}

View File

@@ -0,0 +1,11 @@
package org.signal.apng.ui.theme
import androidx.compose.ui.graphics.Color
val Purple80 = Color(0xFFD0BCFF)
val PurpleGrey80 = Color(0xFFCCC2DC)
val Pink80 = Color(0xFFEFB8C8)
val Purple40 = Color(0xFF6650a4)
val PurpleGrey40 = Color(0xFF625b71)
val Pink40 = Color(0xFF7D5260)

View File

@@ -0,0 +1,75 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.apng.ui.theme
import android.app.Activity
import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.darkColorScheme
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.material3.lightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalView
import androidx.core.view.WindowCompat
private val DarkColorScheme = darkColorScheme(
primary = Purple80,
secondary = PurpleGrey80,
tertiary = Pink80
)
private val LightColorScheme = lightColorScheme(
primary = Purple40,
secondary = PurpleGrey40,
tertiary = Pink40
/* Other default colors to override
background = Color(0xFFFFFBFE),
surface = Color(0xFFFFFBFE),
onPrimary = Color.White,
onSecondary = Color.White,
onTertiary = Color.White,
onBackground = Color(0xFF1C1B1F),
onSurface = Color(0xFF1C1B1F),
*/
)
@Composable
fun SignalTheme(
darkTheme: Boolean = isSystemInDarkTheme(),
// Dynamic color is available on Android 12+
dynamicColor: Boolean = true,
content: @Composable () -> Unit
) {
val colorScheme = when {
dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> {
val context = LocalContext.current
if (darkTheme) dynamicDarkColorScheme(context) else dynamicLightColorScheme(context)
}
darkTheme -> DarkColorScheme
else -> LightColorScheme
}
val view = LocalView.current
if (!view.isInEditMode) {
SideEffect {
val window = (view.context as Activity).window
window.statusBarColor = colorScheme.primary.toArgb()
WindowCompat.getInsetsController(window, view).isAppearanceLightStatusBars = darkTheme
}
}
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.apng.ui.theme
import androidx.compose.material3.Typography
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.sp
// Set of Material typography styles to start with
val Typography = Typography(
bodyLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 16.sp,
lineHeight = 24.sp,
letterSpacing = 0.5.sp
)
/* Other default text styles to override
titleLarge = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Normal,
fontSize = 22.sp,
lineHeight = 28.sp,
letterSpacing = 0.sp
),
labelSmall = TextStyle(
fontFamily = FontFamily.Default,
fontWeight = FontWeight.Medium,
fontSize = 11.sp,
lineHeight = 16.sp,
letterSpacing = 0.5.sp
)
*/
)

View File

@@ -0,0 +1,35 @@
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:aapt="http://schemas.android.com/aapt"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z">
<aapt:attr name="android:fillColor">
<gradient
android:endX="85.84757"
android:endY="92.4963"
android:startX="42.9492"
android:startY="49.59793"
android:type="linear">
<item
android:color="#44000000"
android:offset="0.0" />
<item
android:color="#00000000"
android:offset="1.0" />
</gradient>
</aapt:attr>
</path>
<path
android:fillColor="#FFFFFF"
android:fillType="nonZero"
android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z"
android:strokeWidth="1"
android:strokeColor="#00000000" />
</vector>

View File

@@ -0,0 +1,175 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#3DDC84"
android:pathData="M0,0h108v108h-108z" />
<path
android:fillColor="#00000000"
android:pathData="M9,0L9,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,0L19,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,0L29,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,0L39,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,0L49,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,0L59,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,0L69,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,0L79,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M89,0L89,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M99,0L99,108"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,9L108,9"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,19L108,19"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,29L108,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,39L108,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,49L108,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,59L108,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,69L108,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,79L108,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,89L108,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M0,99L108,99"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,29L89,29"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,39L89,39"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,49L89,49"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,59L89,59"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,69L89,69"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M19,79L89,79"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M29,19L29,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M39,19L39,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M49,19L49,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M59,19L59,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M69,19L69,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
<path
android:fillColor="#00000000"
android:pathData="M79,19L79,89"
android:strokeWidth="0.8"
android:strokeColor="#33FFFFFF" />
</vector>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".DemoActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</LinearLayout>

View File

@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp"
android:padding="8dp">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:text="@string/apng_description_placeholder"/>
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content" />
</LinearLayout>

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical"
android:layout_marginBottom="8dp"
android:padding="8dp">
<TextView
android:id="@+id/description"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="4dp"
android:textAppearance="@android:style/TextAppearance.Material.Headline"
tools:text="Some title"/>
</LinearLayout>

View File

@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"
android:gravity="center_vertical"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".DemoActivity">
<Button
android:id="@+id/demo_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/apng_demo" />
<Button
android:id="@+id/player_button"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/apng_player" />
</LinearLayout>

View File

@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
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"
android:orientation="vertical"
android:gravity="center"
xmlns:tools="http://schemas.android.com/tools"
tools:context=".DemoActivity">
<ImageView
android:id="@+id/player_image"
android:layout_width="300dp"
android:layout_height="300dp" />
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:orientation="horizontal">
<Button
android:id="@+id/prev_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/apng_prev" />
<Button
android:id="@+id/next_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/apng_next" />
</LinearLayout>
<TextView
android:id="@+id/frame_metadata"
android:layout_width="250dp"
android:layout_height="wrap_content"
tools:text="10/15" />
</LinearLayout>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

View File

@@ -0,0 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_foreground" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 982 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,15 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<color name="purple_200">#FFBB86FC</color>
<color name="purple_500">#FF6200EE</color>
<color name="purple_700">#FF3700B3</color>
<color name="teal_200">#FF03DAC5</color>
<color name="teal_700">#FF018786</color>
<color name="black">#FF000000</color>
<color name="white">#FFFFFFFF</color>
</resources>

View File

@@ -0,0 +1,13 @@
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<string name="app_name">apng-app</string>
<string name="apng_description_placeholder">idk</string>
<string name="apng_demo">Demo</string>
<string name="apng_player">Player</string>
<string name="apng_prev">Prev</string>
<string name="apng_next">Next</string>
</resources>

View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright 2024 Signal Messenger, LLC
~ SPDX-License-Identifier: AGPL-3.0-only
-->
<resources>
<style name="Theme.Signal" parent="android:Theme.Material.Light.NoActionBar" />
</resources>

View File

@@ -0,0 +1,21 @@
/*
* Copyright 2024 Signal Messenger, LLC
* SPDX-License-Identifier: AGPL-3.0-only
*/
package org.signal.apng
import org.junit.Assert.assertEquals
import org.junit.Test
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}

1
lib/apng/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/build

13
lib/apng/build.gradle.kts Normal file
View File

@@ -0,0 +1,13 @@
plugins {
id("signal-library")
}
android {
namespace = "org.signal.apng"
}
dependencies {
implementation(project(":core:util"))
testImplementation(testLibs.junit.junit)
testImplementation(testLibs.robolectric.robolectric)
}

View File

21
lib/apng/proguard-rules.pro vendored Normal file
View File

@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile

Some files were not shown because too many files have changed in this diff Show More