feat(release): prepare 1.1.0 candidate

This commit is contained in:
Yun Chan 2026-08-29 18:33:45 +09:00
parent 5a34f66981
commit 5205dcdfa9
736 changed files with 115667 additions and 12203 deletions

View file

@ -0,0 +1,9 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<provider
android:name="com.d3ro.voice.IncomingMediaTestProvider"
android:authorities="com.d3ro.voice.incoming-media-test"
android:exported="true"
android:grantUriPermissions="true" />
</application>
</manifest>

View file

@ -0,0 +1,51 @@
package com.d3ro.voice
import android.util.Base64
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Assert.assertEquals
import org.junit.Assert.assertFalse
import org.junit.Assert.assertTrue
import org.junit.Test
import org.junit.runner.RunWith
import org.linusu.RNGetRandomValuesModule
@RunWith(AndroidJUnit4::class)
class CsprngInstrumentedTest {
@Test
fun nativeEntropyMeetsUuidSourceContract() {
val module = RNGetRandomValuesModule(null)
val first = Base64.decode(module.getRandomBase64(16.0), Base64.NO_WRAP)
val second = Base64.decode(module.getRandomBase64(16.0), Base64.NO_WRAP)
assertEquals(16, first.size)
assertEquals(16, second.size)
assertTrue(first.any { it.toInt() != 0 })
assertTrue(second.any { it.toInt() != 0 })
assertFalse(first.contentEquals(second))
assertTrue(UUID_V4_PATTERN.matches(formatUuidV4(first)))
assertTrue(UUID_V4_PATTERN.matches(formatUuidV4(second)))
}
private fun formatUuidV4(source: ByteArray): String {
require(source.size == 16)
val bytes = source.copyOf()
bytes[6] = ((bytes[6].toInt() and 0x0f) or 0x40).toByte()
bytes[8] = ((bytes[8].toInt() and 0x3f) or 0x80).toByte()
val hex = bytes.joinToString("") { "%02x".format(it.toInt() and 0xff) }
return listOf(
hex.substring(0, 8),
hex.substring(8, 12),
hex.substring(12, 16),
hex.substring(16, 20),
hex.substring(20),
).joinToString("-")
}
private companion object {
val UUID_V4_PATTERN = Regex(
"^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$",
)
}
}

View file

@ -0,0 +1,100 @@
package com.d3ro.voice
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.SystemClock
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.After
import org.junit.Assert.assertEquals
import org.junit.Assert.assertNotNull
import org.junit.Assert.assertNull
import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import java.io.File
@RunWith(AndroidJUnit4::class)
class IncomingMediaInstrumentedTest {
private lateinit var context: Context
@Before
fun clearDelivery() {
context = ApplicationProvider.getApplicationContext()
removePendingFiles()
}
@After
fun cleanupDelivery() {
removePendingFiles()
}
@Test
fun slowProviderNeverBlocksCaptureThreadAndCompletesOnWorker() {
val intent = sharedIntent("slow-audio", "audio/*")
val started = SystemClock.elapsedRealtime()
D3ROIncomingMediaIntents.capture(context, intent)
val captureElapsed = SystemClock.elapsedRealtime() - started
assertTrue("capture blocked for ${captureElapsed}ms", captureElapsed < 200L)
val pending = awaitDelivery()
assertTrue(pending is D3ROIncomingMediaIntents.PendingImport)
pending as D3ROIncomingMediaIntents.PendingImport
assertEquals("지연된 공유 음성.wav", pending.fileName)
assertEquals("audio/wav", pending.mimeType)
assertTrue(pending.path.startsWith("${context.cacheDir.absolutePath}${File.separator}d3ro-incoming-media${File.separator}"))
assertTrue(File(pending.path).isFile)
}
@Test
fun providerSecurityExceptionBecomesOneShotPublicFailure() {
D3ROIncomingMediaIntents.capture(context, sharedIntent("throwing", "audio/*"))
val pending = awaitDelivery()
assertTrue(pending is D3ROIncomingMediaIntents.PendingFailure)
pending as D3ROIncomingMediaIntents.PendingFailure
assertEquals("provider-unavailable", pending.errorCode)
assertEquals(pending, D3ROIncomingMediaIntents.consume(context))
assertNull(D3ROIncomingMediaIntents.consume(context))
}
@Test
fun unadvertisedVideoMimeBecomesVisibleUnsupportedFailure() {
D3ROIncomingMediaIntents.capture(
context,
sharedIntent("unsupported-video", "video/webm"),
)
val pending = awaitDelivery()
assertTrue(pending is D3ROIncomingMediaIntents.PendingFailure)
pending as D3ROIncomingMediaIntents.PendingFailure
assertEquals("unsupported-mime", pending.errorCode)
assertNotNull(D3ROIncomingMediaIntents.consume(context))
assertNull(D3ROIncomingMediaIntents.consume(context))
}
private fun sharedIntent(path: String, mimeType: String): Intent = Intent(Intent.ACTION_SEND).apply {
type = mimeType
putExtra(Intent.EXTRA_STREAM, Uri.parse("content://${IncomingMediaTestProvider.AUTHORITY}/$path"))
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
}
private fun awaitDelivery(): D3ROIncomingMediaIntents.PendingDelivery {
val deadline = SystemClock.elapsedRealtime() + 5_000L
while (SystemClock.elapsedRealtime() < deadline) {
D3ROIncomingMediaIntents.peek(context)?.let { return it }
SystemClock.sleep(20L)
}
throw AssertionError("incoming media delivery timed out")
}
private fun removePendingFiles() {
repeat(4) {
val pending = D3ROIncomingMediaIntents.consume(context) ?: return
if (pending is D3ROIncomingMediaIntents.PendingImport) File(pending.path).delete()
}
}
}

View file

@ -0,0 +1,97 @@
package com.d3ro.voice;
import android.content.ContentProvider;
import android.content.ContentValues;
import android.database.Cursor;
import android.database.MatrixCursor;
import android.net.Uri;
import android.os.ParcelFileDescriptor;
import android.provider.OpenableColumns;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
/** Hostile/slow provider used only by the Android instrumentation contract. */
public final class IncomingMediaTestProvider extends ContentProvider {
public static final String AUTHORITY = "com.d3ro.voice.incoming-media-test";
private static final long SLOW_PROVIDER_MS = 800L;
private static final byte[] WAV_BYTES = new byte[] {
'R', 'I', 'F', 'F', 4, 0, 0, 0, 'W', 'A', 'V', 'E'
};
@Override
public boolean onCreate() {
return true;
}
@Override
public String getType(Uri uri) {
String path = uri.getLastPathSegment();
if ("slow-audio".equals(path)) {
try {
Thread.sleep(SLOW_PROVIDER_MS);
} catch (InterruptedException error) {
Thread.currentThread().interrupt();
return null;
}
return "audio/wav";
}
if ("throwing".equals(path)) {
throw new SecurityException("test provider denied metadata");
}
if ("unsupported-video".equals(path)) {
return "video/webm";
}
return null;
}
@Override
public Cursor query(
Uri uri,
String[] projection,
String selection,
String[] selectionArgs,
String sortOrder) {
MatrixCursor cursor = new MatrixCursor(
new String[] {OpenableColumns.DISPLAY_NAME, OpenableColumns.SIZE}
);
cursor.addRow(new Object[] {"지연된 공유 음성.wav", (long) WAV_BYTES.length});
return cursor;
}
@Override
public ParcelFileDescriptor openFile(Uri uri, String mode) throws FileNotFoundException {
if (!"slow-audio".equals(uri.getLastPathSegment())) {
throw new SecurityException("test provider denied bytes");
}
if (getContext() == null) {
throw new FileNotFoundException("test provider context unavailable");
}
File file = new File(getContext().getCacheDir(), "incoming-media-provider-test.wav");
try (FileOutputStream output = new FileOutputStream(file)) {
output.write(WAV_BYTES);
} catch (IOException error) {
FileNotFoundException wrapped = new FileNotFoundException("test provider fixture unavailable");
wrapped.initCause(error);
throw wrapped;
}
return ParcelFileDescriptor.open(file, ParcelFileDescriptor.MODE_READ_ONLY);
}
@Override
public Uri insert(Uri uri, ContentValues values) {
return null;
}
@Override
public int delete(Uri uri, String selection, String[] selectionArgs) {
return 0;
}
@Override
public int update(Uri uri, ContentValues values, String selection, String[] selectionArgs) {
return 0;
}
}