feat(release): prepare 1.1.0 candidate
This commit is contained in:
parent
5a34f66981
commit
5205dcdfa9
736 changed files with 115667 additions and 12203 deletions
|
|
@ -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>
|
||||
|
|
@ -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}$",
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue