From 21f95126885136ccf1d46af89db4f0460f7ae3aa Mon Sep 17 00:00:00 2001 From: Oupson Date: Fri, 25 Jun 2021 16:34:25 +0200 Subject: [PATCH] Work on app --- apng_library/build.gradle | 4 +- apng_library/src/main/AndroidManifest.xml | 1 + .../java/oupson/apng/decoder/ApngDecoder.kt | 15 +- .../java/oupson/apng/decoder/ApngLoader.kt | 133 ++++++------- .../java/oupson/apng/encoder/ApngEncoder.kt | 2 +- .../src/main/java/oupson/apng/utils/Loader.kt | 9 +- .../src/main/java/oupson/apng/utils/Utils.kt | 11 +- app-test/build.gradle | 15 +- .../apngcreator/activities/CreatorActivity.kt | 177 +++++++++--------- .../apngcreator/activities/MainActivity.kt | 116 ++++++++---- .../apngcreator/activities/ViewerActivity.kt | 35 ++-- .../apngcreator/adapter/ImageAdapter.kt | 22 ++- .../apngcreator/fragments/KotlinFragment.kt | 9 +- app-test/src/main/res/layout/dialog_delay.xml | 8 +- .../main/res/layout/fragment_apng_decoder.xml | 3 +- .../src/main/res/layout/fragment_java.xml | 3 +- .../src/main/res/layout/fragment_kotlin.xml | 4 +- app-test/src/main/res/layout/list_image.xml | 3 +- build.gradle | 3 +- 19 files changed, 323 insertions(+), 250 deletions(-) diff --git a/apng_library/build.gradle b/apng_library/build.gradle index 038ac45..655d275 100644 --- a/apng_library/build.gradle +++ b/apng_library/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.library' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { @@ -29,8 +28,7 @@ android { } dependencies { - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' implementation 'androidx.appcompat:appcompat:1.3.0' diff --git a/apng_library/src/main/AndroidManifest.xml b/apng_library/src/main/AndroidManifest.xml index a49ba4d..e7377fa 100644 --- a/apng_library/src/main/AndroidManifest.xml +++ b/apng_library/src/main/AndroidManifest.xml @@ -1,2 +1,3 @@ + diff --git a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt index ff3a7a0..ef8dad0 100644 --- a/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt +++ b/apng_library/src/main/java/oupson/apng/decoder/ApngDecoder.kt @@ -71,6 +71,7 @@ class ApngDecoder(input: InputStream, val config: Config) { val inputStream = BufferedInputStream(inputStream) val bytes = ByteArray(8) inputStream.mark(8) + withContext(Dispatchers.IO) { inputStream.read(bytes) } @@ -108,7 +109,6 @@ class ApngDecoder(input: InputStream, val config: Config) { if (withContext(Dispatchers.IO) { byteRead = inputStream.read(lengthChunk) - if (byteRead != -1) { length = Utils.uIntFromBytesBigEndian(lengthChunk) @@ -149,6 +149,7 @@ class ApngDecoder(input: InputStream, val config: Config) { ) } } + cover?.close() cover = null } else { // Add IEND body length : 0 @@ -234,9 +235,9 @@ class ApngDecoder(input: InputStream, val config: Config) { } else -> buffer = btm } - } + png?.close() png = ByteArrayOutputStream(4096) // Parse Frame ConTroL chunk @@ -318,6 +319,7 @@ class ApngDecoder(input: InputStream, val config: Config) { ) val pngBytes = png.toByteArray() + png.close() val decoded = BitmapFactory.decodeByteArray( pngBytes, 0, @@ -393,11 +395,14 @@ class ApngDecoder(input: InputStream, val config: Config) { crC32.update(Utils.IEND, 0, Utils.IEND.size) it.write(Utils.IEND) it.write(Utils.uIntToByteArray(crC32.value.toInt())) + withContext(Dispatchers.IO) { inputStream.close() } val pngBytes = it.toByteArray() + it.close() + return@withContext BitmapDrawable( context.resources, BitmapFactory.decodeByteArray( @@ -530,16 +535,14 @@ class ApngDecoder(input: InputStream, val config: Config) { suspend fun getDecoded(context: Context): Result { if (result == null) { - result = - decodeApng(context) - + result = decodeApng(context) kotlin.runCatching { withContext(Dispatchers.IO) { inputStream?.close() } }.onFailure { - return Result.failure(it) + this.result = Result.failure(it) } inputStream = null diff --git a/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt b/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt index 20d46eb..3c46fb4 100644 --- a/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt +++ b/apng_library/src/main/java/oupson/apng/decoder/ApngLoader.kt @@ -10,9 +10,9 @@ import android.widget.ImageView import androidx.annotation.RawRes import kotlinx.coroutines.* import oupson.apng.drawable.ApngDrawable +import oupson.apng.utils.Utils.Companion.mapResult import java.io.File import java.io.FileInputStream -import java.io.FileNotFoundException import java.net.URL class ApngLoader(parent: Job? = null) { @@ -50,18 +50,18 @@ class ApngLoader(parent: Job? = null) { file: File, imageView: ImageView, config: ApngDecoder.Config = ApngDecoder.Config() - ): Result { - val result = + ): Result = + kotlin.runCatching { + withContext(Dispatchers.IO) { + FileInputStream(file) + } + }.mapResult { input -> ApngDecoder( - withContext(Dispatchers.IO) { - FileInputStream(file) - }, + input, config ).getDecoded(context) - - if (result.isSuccess) { + }.onSuccess { drawable -> withContext(Dispatchers.Main) { - val drawable = result.getOrNull() imageView.setImageDrawable(drawable) (drawable as? AnimationDrawable)?.start() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -69,8 +69,6 @@ class ApngLoader(parent: Job? = null) { } } } - return result - } /** * Load Apng into an imageView. @@ -84,19 +82,16 @@ class ApngLoader(parent: Job? = null) { uri: Uri, imageView: ImageView, config: ApngDecoder.Config = ApngDecoder.Config() - ): Result { - val inputStream = - withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) } - ?: throw FileNotFoundException("Failed to load $uri") // TODO Result - val result = + ): Result = + kotlin.runCatching { + withContext(Dispatchers.IO) { context.contentResolver.openInputStream(uri) }!! + }.mapResult { inputStream -> ApngDecoder( inputStream, config ).getDecoded(context) - - if (result.isSuccess) { + }.onSuccess { drawable -> withContext(Dispatchers.Main) { - val drawable = result.getOrNull() imageView.setImageDrawable(drawable) (drawable as? AnimationDrawable)?.start() if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { @@ -104,8 +99,7 @@ class ApngLoader(parent: Job? = null) { } } } - return result - } + /** * Load Apng into an imageView. @@ -118,27 +112,23 @@ class ApngLoader(parent: Job? = null) { context: Context, @RawRes res: Int, imageView: ImageView, config: ApngDecoder.Config = ApngDecoder.Config() - ): Result { - val result = - ApngDecoder( - withContext(Dispatchers.IO) { - context.resources.openRawResource(res) - }, - config - ).getDecoded(context) - - if (result.isSuccess) { - withContext(Dispatchers.Main) { - val drawable = result.getOrNull() - imageView.setImageDrawable(drawable) - (drawable as? AnimationDrawable)?.start() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - (drawable as? AnimatedImageDrawable)?.start() + ): Result = + ApngDecoder( + withContext(Dispatchers.IO) { + context.resources.openRawResource(res) + }, + config + ).getDecoded(context) + .onSuccess { drawable -> + withContext(Dispatchers.Main) { + imageView.setImageDrawable(drawable) + (drawable as? AnimationDrawable)?.start() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + (drawable as? AnimatedImageDrawable)?.start() + } } } - } - return result - } + /** * Load Apng into an imageView, asynchronously. @@ -152,23 +142,18 @@ class ApngLoader(parent: Job? = null) { url: URL, imageView: ImageView, config: ApngDecoder.Config = ApngDecoder.Config() - ): Result { - val result = - ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) } - .getDecoded(context) - if (result.isSuccess) { - withContext(Dispatchers.Main) { - val drawable = result.getOrNull() - imageView.setImageDrawable(drawable) - (drawable as? AnimationDrawable)?.start() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - (drawable as? AnimatedImageDrawable)?.start() + ): Result = + ApngDecoder.constructFromUrl(url, config).getOrElse { return Result.failure(it) } + .getDecoded(context) + .onSuccess { drawable -> + withContext(Dispatchers.Main) { + imageView.setImageDrawable(drawable) + (drawable as? AnimationDrawable)?.start() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + (drawable as? AnimatedImageDrawable)?.start() + } } } - } - - return result - } /** * Load Apng into an imageView, asynchronously. @@ -185,12 +170,16 @@ class ApngLoader(parent: Job? = null) { config: ApngDecoder.Config = ApngDecoder.Config() ): Result { return if (string.startsWith("http://") || string.startsWith("https://")) { - decodeApngInto( - context, - URL(string), - imageView, - config - ) + kotlin.runCatching { URL(string) } + .mapResult { url -> + decodeApngInto( + context, + url, + imageView, + config + ) + } + } else if (File(string).exists()) { var pathToLoad = if (string.startsWith("content://")) string else "file://$string" @@ -202,25 +191,21 @@ class ApngLoader(parent: Job? = null) { config ) } else if (string.startsWith("file://android_asset/")) { - val inputStream = kotlin.runCatching { + kotlin.runCatching { withContext(Dispatchers.IO) { context.assets.open(string.replace("file:///android_asset/", "")) } - }.getOrElse { - return Result.failure(it) } - val result = ApngDecoder(inputStream, config).getDecoded(context) - if (result.isSuccess) { - withContext(Dispatchers.Main) { - val drawable = result.getOrNull() - imageView.setImageDrawable(drawable) - (drawable as? AnimationDrawable)?.start() - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { - (drawable as? AnimatedImageDrawable)?.start() + .mapResult { inputStream -> ApngDecoder(inputStream, config).getDecoded(context) } + .onSuccess { drawable -> + withContext(Dispatchers.Main) { + imageView.setImageDrawable(drawable) + (drawable as? AnimationDrawable)?.start() + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) { + (drawable as? AnimatedImageDrawable)?.start() + } } } - } - result } else { throw Exception("Cannot open string") } diff --git a/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt b/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt index 113de2b..66c9d0a 100644 --- a/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt +++ b/apng_library/src/main/java/oupson/apng/encoder/ApngEncoder.kt @@ -291,7 +291,7 @@ class ApngEncoder( ) { if (currentFrame == 0) { if (btm.width != width || btm.height != height) - throw InvalidFrameSizeException( + throw InvalidFrameSizeException( // TODO btm.width, btm.height, width, diff --git a/apng_library/src/main/java/oupson/apng/utils/Loader.kt b/apng_library/src/main/java/oupson/apng/utils/Loader.kt index 775008b..ddfe8af 100644 --- a/apng_library/src/main/java/oupson/apng/utils/Loader.kt +++ b/apng_library/src/main/java/oupson/apng/utils/Loader.kt @@ -24,8 +24,11 @@ class Loader { val connection = url.openConnection() as HttpURLConnection connection.useCaches = true connection.connect() + + val inputStream = connection.inputStream + if (connection.responseCode == 200) { - val input = BufferedInputStream(connection.inputStream) + val input = BufferedInputStream(inputStream) val output = ByteArrayOutputStream() var bytesRead: Int val buffer = ByteArray(4096) @@ -36,9 +39,13 @@ class Loader { } while (bytesRead != -1) input.close() output.close() + + inputStream.close() connection.disconnect() + output.toByteArray() } else { + inputStream.close() connection.disconnect() throw Exception("Error when downloading file : ${connection.responseCode}") } diff --git a/apng_library/src/main/java/oupson/apng/utils/Utils.kt b/apng_library/src/main/java/oupson/apng/utils/Utils.kt index 10d861f..d8de186 100644 --- a/apng_library/src/main/java/oupson/apng/utils/Utils.kt +++ b/apng_library/src/main/java/oupson/apng/utils/Utils.kt @@ -208,7 +208,7 @@ class Utils { (bytes[1] and 0xFF)) // TODO DOCUMENT AND TEST - fun uShortFromBytesBigEndian(bytes: ByteArray, offset : Int = 0): Int = + fun uShortFromBytesBigEndian(bytes: ByteArray, offset: Int = 0): Int = (((bytes[offset].toInt() and 0xFF) shl 8) or (bytes[offset + 1].toInt() and 0xFF)) @@ -330,5 +330,14 @@ class Utils { } return result } + + suspend fun Result.mapResult(block: suspend (T) -> Result): Result { + return this.fold( + onSuccess = { + block.invoke(it) + }, + onFailure = { Result.failure(it) } + ) + } } } \ No newline at end of file diff --git a/app-test/build.gradle b/app-test/build.gradle index ce736ef..0beeee8 100644 --- a/app-test/build.gradle +++ b/app-test/build.gradle @@ -1,6 +1,5 @@ apply plugin: 'com.android.application' apply plugin: 'kotlin-android' -apply plugin: 'kotlin-android-extensions' android { compileSdkVersion 30 @@ -24,29 +23,35 @@ android { buildTypes { release { - minifyEnabled false + minifyEnabled true + shrinkResources true proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' } } productFlavors { } + + buildFeatures { + viewBinding = true + } + } dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.4.3' + implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.5.0' implementation 'androidx.appcompat:appcompat:1.3.0' implementation 'androidx.constraintlayout:constraintlayout:2.0.4' implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation 'com.google.android.material:material:1.3.0' + implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.3.1' testImplementation 'junit:junit:4.13.2' androidTestImplementation 'androidx.test:runner:1.3.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0' - implementation 'com.squareup.picasso:picasso:2.71828' + implementation("io.coil-kt:coil:1.2.2") implementation project(":apng_library") // implementation fileTree(include: ['*.aar'], dir: 'libs') diff --git a/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt b/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt index 50427ea..8b6dde6 100644 --- a/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt +++ b/app-test/src/main/java/oupson/apngcreator/activities/CreatorActivity.kt @@ -14,18 +14,18 @@ import android.util.Log import android.view.Menu import android.view.MenuItem import android.widget.Toast +import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.app.NotificationCompat import androidx.core.app.NotificationManagerCompat import androidx.core.content.FileProvider +import androidx.lifecycle.lifecycleScope import androidx.recyclerview.widget.DefaultItemAnimator import androidx.recyclerview.widget.ItemTouchHelper import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.RecyclerView import com.google.android.material.snackbar.Snackbar -import kotlinx.android.synthetic.main.activity_creator.* import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import oupson.apng.encoder.ApngEncoder @@ -33,6 +33,7 @@ import oupson.apng.utils.Utils import oupson.apngcreator.BuildConfig import oupson.apngcreator.R import oupson.apngcreator.adapter.ImageAdapter +import oupson.apngcreator.databinding.ActivityCreatorBinding import oupson.apngcreator.dialogs.DelayInputDialog import java.io.File import java.io.FileOutputStream @@ -42,8 +43,6 @@ import kotlin.collections.ArrayList class CreatorActivity : AppCompatActivity() { companion object { - private const val PICK_IMAGE = 1 - private const val WRITE_REQUEST_CODE = 2 private const val TAG = "CreatorActivity" private const val CREATION_CHANNEL_ID = @@ -57,35 +56,97 @@ class CreatorActivity : AppCompatActivity() { private var nextImageId: Long = 0 + private var binding: ActivityCreatorBinding? = null + + private val pickLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data: Intent? = result.data + + if (data?.clipData != null) { + for (i in 0 until data.clipData!!.itemCount) { + items.add(Triple(data.clipData!!.getItemAt(i).uri, 1000, nextImageId++)) + } + adapter?.notifyDataSetChanged() + } else if (data?.data != null) { + items.add(Triple(data.data!!, 1000, nextImageId++)) + adapter?.notifyDataSetChanged() + } + } + } + + private val writeLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == Activity.RESULT_OK) { + val data: Intent? = result.data + + if (data?.data != null) { + if (BuildConfig.DEBUG) + Log.i(TAG, "Intent data : ${data.data}") + + val builder = NotificationCompat.Builder(this, CREATION_CHANNEL_ID).apply { + setContentTitle(getString(R.string.create_notification_title)) + setContentText( + this@CreatorActivity.resources.getQuantityString( + R.plurals.create_notification_description, + 0, + 0, + items.size + ) + ) + setSmallIcon(R.drawable.ic_create_white_24dp) + priority = NotificationCompat.PRIORITY_LOW + } + lifecycleScope.launch(Dispatchers.IO) { + val out = contentResolver.openOutputStream(data.data!!) ?: return@launch + saveToOutputStream( + items.map { Pair(it.first, it.second) }, + out, + builder = builder + ) + out.close() + + if (binding != null) { + withContext(Dispatchers.Main) { + Snackbar.make( + binding!!.imageRecyclerView, + R.string.done, + Snackbar.LENGTH_SHORT + ).show() + } + } + } + } + } + } + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + binding = ActivityCreatorBinding.inflate(layoutInflater) - setContentView(R.layout.activity_creator) + setContentView(binding?.root) - fabAddImage.setOnClickListener { + binding?.fabAddImage?.setOnClickListener { val getIntent = Intent(Intent.ACTION_GET_CONTENT) getIntent.putExtra(Intent.EXTRA_ALLOW_MULTIPLE, true) getIntent.type = "image/*" - startActivityForResult( - getIntent, - PICK_IMAGE - ) + pickLauncher.launch(getIntent) } - adapter = ImageAdapter(this, items) + adapter = ImageAdapter(this, items, lifecycleScope) adapter?.setHasStableIds(true) - imageRecyclerView.layoutManager = LinearLayoutManager(this) - imageRecyclerView.setHasFixedSize(true) - imageRecyclerView.itemAnimator = object : DefaultItemAnimator() { + binding?.imageRecyclerView?.layoutManager = LinearLayoutManager(this) + binding?.imageRecyclerView?.setHasFixedSize(true) + binding?.imageRecyclerView?.itemAnimator = object : DefaultItemAnimator() { override fun canReuseUpdatedViewHolder(viewHolder: RecyclerView.ViewHolder): Boolean { return true } } - imageRecyclerView.setItemViewCacheSize(20) + binding?.imageRecyclerView?.setItemViewCacheSize(20) if (adapter != null) - ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(imageRecyclerView) + ItemTouchHelper(SwipeToDeleteCallback(adapter!!)).attachToRecyclerView(binding?.imageRecyclerView) adapter?.clickListener = { position -> DelayInputDialog(object : DelayInputDialog.InputSenderDialogListener { @@ -104,8 +165,8 @@ class CreatorActivity : AppCompatActivity() { }, items[position].second).show(supportFragmentManager, null) } - setSupportActionBar(creatorBottomAppBar) - imageRecyclerView.adapter = adapter + setSupportActionBar(binding?.creatorBottomAppBar) + binding?.imageRecyclerView?.adapter = adapter supportActionBar?.setDisplayHomeAsUpEnabled(true) if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { @@ -148,7 +209,7 @@ class CreatorActivity : AppCompatActivity() { priority = NotificationCompat.PRIORITY_LOW } - GlobalScope.launch(Dispatchers.IO) { + lifecycleScope.launch(Dispatchers.IO) { val randomFileName = UUID.randomUUID().toString() val f = File(filesDir, "images/$randomFileName.png").apply { if (!exists()) { @@ -205,7 +266,7 @@ class CreatorActivity : AppCompatActivity() { setSmallIcon(R.drawable.ic_create_white_24dp) priority = NotificationCompat.PRIORITY_LOW } - GlobalScope.launch(Dispatchers.IO) { + lifecycleScope.launch(Dispatchers.IO) { val randomFileName = UUID.randomUUID().toString() val f = File(filesDir, "images/$randomFileName.png").apply { if (!exists()) { @@ -272,7 +333,10 @@ class CreatorActivity : AppCompatActivity() { type = "image/png" putExtra(Intent.EXTRA_TITLE, "${items[0].first.lastPathSegment}.png") } - startActivityForResult(intent, WRITE_REQUEST_CODE) + + writeLauncher.launch( + intent + ) } true } @@ -409,7 +473,9 @@ class CreatorActivity : AppCompatActivity() { } } - encoder.writeEnd() + withContext(Dispatchers.IO) { + encoder.writeEnd() + } if (builder != null) { @@ -423,71 +489,14 @@ class CreatorActivity : AppCompatActivity() { } - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - super.onActivityResult(requestCode, resultCode, data) - when (requestCode) { - PICK_IMAGE -> { - if (resultCode == Activity.RESULT_OK) { - if (data?.clipData != null) { - for (i in 0 until data.clipData!!.itemCount) { - items.add(Triple(data.clipData!!.getItemAt(i).uri, 1000, nextImageId++)) - } - adapter?.notifyDataSetChanged() - } else if (data?.data != null) { - items.add(Triple(data.data!!, 1000, nextImageId++)) - adapter?.notifyDataSetChanged() - } - } - } - WRITE_REQUEST_CODE -> { - if (resultCode == Activity.RESULT_OK) { - if (data?.data != null) { - if (BuildConfig.DEBUG) - Log.i(TAG, "Intent data : ${data.data}") - - val builder = NotificationCompat.Builder(this, CREATION_CHANNEL_ID).apply { - setContentTitle(getString(R.string.create_notification_title)) - setContentText( - this@CreatorActivity.resources.getQuantityString( - R.plurals.create_notification_description, - 0, - 0, - items.size - ) - ) - setSmallIcon(R.drawable.ic_create_white_24dp) - priority = NotificationCompat.PRIORITY_LOW - } - GlobalScope.launch(Dispatchers.IO) { - - val out = contentResolver.openOutputStream(data.data!!) ?: return@launch - saveToOutputStream( - items.map { Pair(it.first, it.second) }, - out, - builder = builder - ) - out.close() - - withContext(Dispatchers.Main) { - Snackbar.make( - imageRecyclerView, - R.string.done, - Snackbar.LENGTH_SHORT - ).show() - } - } - } - } - } - } - } - override fun onDestroy() { super.onDestroy() - val deleteResult = File(filesDir, "images").deleteRecursively() - if (BuildConfig.DEBUG) - Log.v(TAG, "Deleted images dir : $deleteResult") + lifecycleScope.launch(Dispatchers.IO) { + val deleteResult = File(filesDir, "images").deleteRecursively() + if (BuildConfig.DEBUG) + Log.v(TAG, "Deleted images dir : $deleteResult") + } } inner class SwipeToDeleteCallback(private val adapter: ImageAdapter) : diff --git a/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt b/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt index 7b49170..d7ac924 100644 --- a/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt +++ b/app-test/src/main/java/oupson/apngcreator/activities/MainActivity.kt @@ -4,19 +4,25 @@ import android.annotation.SuppressLint import android.content.Intent import android.net.http.HttpResponseCache import android.os.Bundle +import android.os.StrictMode +import android.os.StrictMode.ThreadPolicy +import android.os.StrictMode.VmPolicy import android.util.Log import android.view.MenuItem import androidx.appcompat.app.ActionBarDrawerToggle import androidx.appcompat.app.AppCompatActivity import androidx.core.view.GravityCompat +import androidx.lifecycle.lifecycleScope import com.google.android.material.bottomappbar.BottomAppBarTopEdgeTreatment import com.google.android.material.shape.CutCornerTreatment import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.ShapeAppearanceModel import com.google.android.material.shape.ShapePath -import kotlinx.android.synthetic.main.activity_main.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch import oupson.apngcreator.BuildConfig import oupson.apngcreator.R +import oupson.apngcreator.databinding.ActivityMainBinding import oupson.apngcreator.fragments.ApngDecoderFragment import oupson.apngcreator.fragments.JavaFragment import oupson.apngcreator.fragments.KotlinFragment @@ -28,38 +34,67 @@ class MainActivity : AppCompatActivity() { private const val TAG = "MainActivity" } + private var binding: ActivityMainBinding? = null + override fun onCreate(savedInstanceState: Bundle?) { + if (BuildConfig.DEBUG) { + StrictMode.setThreadPolicy( + ThreadPolicy.Builder() + .detectDiskReads() + .detectDiskWrites() + .detectAll() + .penaltyLog() + .build() + ) + StrictMode.setVmPolicy( + VmPolicy.Builder() + .detectLeakedSqlLiteObjects() + .detectLeakedClosableObjects() + .penaltyLog() + + .build() + ) + } super.onCreate(savedInstanceState) if (BuildConfig.DEBUG) - Log.v(TAG, "supportFragmentManager.fragments.size : ${supportFragmentManager.fragments.size}") + Log.v( + TAG, + "supportFragmentManager.fragments.size : ${supportFragmentManager.fragments.size}" + ) - setContentView(R.layout.activity_main) + binding = ActivityMainBinding.inflate(layoutInflater) - setSupportActionBar(bottomAppBar) + + setContentView(binding?.root) + + setSupportActionBar(binding?.bottomAppBar) setUpBottomAppBarShapeAppearance() val httpCacheSize = 10 * 1024 * 1024.toLong() // 10 MiB - val httpCacheDir = File(cacheDir, "http") - HttpResponseCache.install(httpCacheDir, httpCacheSize) + lifecycleScope.launch(Dispatchers.IO) { + val httpCacheDir = File(cacheDir, "http") + HttpResponseCache.install(httpCacheDir, httpCacheSize) + } - fabCreate.setOnClickListener { + binding?.fabCreate?.setOnClickListener { startActivity(Intent(this, CreatorActivity::class.java)) } - val drawerToggle = ActionBarDrawerToggle(this, drawer_layout, bottomAppBar, + val drawerToggle = ActionBarDrawerToggle( + this, binding?.drawerLayout, binding?.bottomAppBar, R.string.open, R.string.close ) - drawer_layout.addDrawerListener(drawerToggle) + binding?.drawerLayout?.addDrawerListener(drawerToggle) drawerToggle.syncState() var selected = 0 - navigationView.setNavigationItemSelectedListener { menuItem : MenuItem -> - when(menuItem.itemId) { + binding?.navigationView?.setNavigationItemSelectedListener { menuItem: MenuItem -> + when (menuItem.itemId) { R.id.menu_kotlin_fragment -> { if (selected != 0) { supportFragmentManager.beginTransaction().apply { @@ -98,20 +133,21 @@ class MainActivity : AppCompatActivity() { } } - drawer_layout.closeDrawer(GravityCompat.START) + binding?.drawerLayout?.closeDrawer(GravityCompat.START) return@setNavigationItemSelectedListener true } if (intent.hasExtra("fragment") && supportFragmentManager.fragments.size == 0) { - when(intent.getStringExtra("fragment")) { + when (intent.getStringExtra("fragment")) { "kotlin" -> { supportFragmentManager.beginTransaction().apply { add( R.id.fragment_container, - KotlinFragment.newInstance(), "KotlinFragment") + KotlinFragment.newInstance(), "KotlinFragment" + ) }.commit() - navigationView.setCheckedItem(R.id.menu_kotlin_fragment) + binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment) selected = 0 } "java" -> { @@ -121,7 +157,7 @@ class MainActivity : AppCompatActivity() { JavaFragment() ) }.commit() - navigationView.setCheckedItem(R.id.menu_java_fragment) + binding?.navigationView?.setCheckedItem(R.id.menu_java_fragment) selected = 1 } "apng_decoder" -> { @@ -131,7 +167,7 @@ class MainActivity : AppCompatActivity() { ApngDecoderFragment.newInstance() ) }.commit() - navigationView.setCheckedItem(R.id.menu_apng_decoder_fragment) + binding?.navigationView?.setCheckedItem(R.id.menu_apng_decoder_fragment) selected = 2 } } @@ -139,35 +175,41 @@ class MainActivity : AppCompatActivity() { supportFragmentManager.beginTransaction().apply { add( R.id.fragment_container, - KotlinFragment.newInstance(), "KotlinFragment") + KotlinFragment.newInstance(), "KotlinFragment" + ) }.commit() - navigationView.setCheckedItem(R.id.menu_kotlin_fragment) + binding?.navigationView?.setCheckedItem(R.id.menu_kotlin_fragment) } } override fun onStop() { super.onStop() - HttpResponseCache.getInstalled()?.flush() + lifecycleScope.launch(Dispatchers.IO) { + HttpResponseCache.getInstalled()?.flush() + } } private fun setUpBottomAppBarShapeAppearance() { - val fabShapeAppearanceModel: ShapeAppearanceModel = fabCreate.shapeAppearanceModel - val cutCornersFab = - (fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment - && fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment) - val topEdge = - if (cutCornersFab) BottomAppBarCutCornersTopEdge( - bottomAppBar.fabCradleMargin, - bottomAppBar.fabCradleRoundedCornerRadius, - bottomAppBar.cradleVerticalOffset - ) else BottomAppBarTopEdgeTreatment( - bottomAppBar.fabCradleMargin, - bottomAppBar.fabCradleRoundedCornerRadius, - bottomAppBar.cradleVerticalOffset - ) - val babBackground = bottomAppBar.background as MaterialShapeDrawable - babBackground.shapeAppearanceModel = - babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build() + if (binding != null) { + val fabShapeAppearanceModel: ShapeAppearanceModel = + binding!!.fabCreate.shapeAppearanceModel + val cutCornersFab = + (fabShapeAppearanceModel.bottomLeftCorner is CutCornerTreatment + && fabShapeAppearanceModel.bottomRightCorner is CutCornerTreatment) + val topEdge = + if (cutCornersFab) BottomAppBarCutCornersTopEdge( + binding!!.bottomAppBar.fabCradleMargin, + binding!!.bottomAppBar.fabCradleRoundedCornerRadius, + binding!!.bottomAppBar.cradleVerticalOffset + ) else BottomAppBarTopEdgeTreatment( + binding!!.bottomAppBar.fabCradleMargin, + binding!!.bottomAppBar.fabCradleRoundedCornerRadius, + binding!!.bottomAppBar.cradleVerticalOffset + ) + val babBackground = binding!!.bottomAppBar.background as MaterialShapeDrawable + babBackground.shapeAppearanceModel = + babBackground.shapeAppearanceModel.toBuilder().setTopEdge(topEdge).build() + } } diff --git a/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt b/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt index 715335b..aaad23d 100644 --- a/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt +++ b/app-test/src/main/java/oupson/apngcreator/activities/ViewerActivity.kt @@ -9,16 +9,19 @@ import android.view.View import androidx.appcompat.app.AppCompatActivity import androidx.core.app.ActivityCompat import androidx.core.content.ContextCompat -import kotlinx.android.synthetic.main.activity_viewer.* import oupson.apng.decoder.ApngDecoder import oupson.apng.decoder.ApngLoader -import oupson.apngcreator.R +import oupson.apngcreator.databinding.ActivityViewerBinding class ViewerActivity : AppCompatActivity() { private var apngLoader: ApngLoader? = null + private var binding: ActivityViewerBinding? = null + override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) - setContentView(R.layout.activity_viewer) + binding = ActivityViewerBinding.inflate(layoutInflater) + + setContentView(binding?.root) this.apngLoader = ApngLoader() @@ -54,18 +57,20 @@ class ViewerActivity : AppCompatActivity() { private fun load() { val uri = intent.data ?: return - apngLoader?.decodeApngAsyncInto( - this, - uri, - viewerImageView, - callback = object : ApngLoader.Callback { - override fun onSuccess(drawable: Drawable) {} - override fun onError(error: Throwable) { - Log.e("ViewerActivity", "Error when loading file", error) - } - }, - ApngDecoder.Config(decodeCoverFrame = false) - ) + + if (binding != null) + apngLoader?.decodeApngAsyncInto( + this, + uri, + binding!!.viewerImageView, + callback = object : ApngLoader.Callback { + override fun onSuccess(drawable: Drawable) {} + override fun onError(error: Throwable) { + Log.e("ViewerActivity", "Error when loading file", error) + } + }, + ApngDecoder.Config(decodeCoverFrame = false) + ) } override fun onRequestPermissionsResult( diff --git a/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt b/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt index 7acf900..e0cb56e 100644 --- a/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt +++ b/app-test/src/main/java/oupson/apngcreator/adapter/ImageAdapter.kt @@ -10,21 +10,25 @@ import android.view.ViewGroup import android.widget.ImageView import android.widget.TextView import androidx.recyclerview.widget.RecyclerView +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.launch import kotlinx.coroutines.withContext import oupson.apngcreator.R -class ImageAdapter(private val context : Context, private val list : List>) : RecyclerView.Adapter() { - inner class ImageHolder(view : View) : RecyclerView.ViewHolder(view) { - val imageView : ImageView? = view.findViewById(R.id.listImageView) - val textDelay : TextView? = view.findViewById(R.id.textDelay) - val positionTextView : TextView? = view.findViewById(R.id.position_textView) - val nameTextView : TextView? = view.findViewById(R.id.name_textView) +class ImageAdapter( + private val context: Context, + private val list: List>, + private val scope: CoroutineScope +) : RecyclerView.Adapter() { + inner class ImageHolder(view: View) : RecyclerView.ViewHolder(view) { + val imageView: ImageView? = view.findViewById(R.id.listImageView) + val textDelay: TextView? = view.findViewById(R.id.textDelay) + val positionTextView: TextView? = view.findViewById(R.id.position_textView) + val nameTextView: TextView? = view.findViewById(R.id.name_textView) } - var clickListener : ((position : Int) -> Unit)? = null + var clickListener: ((position: Int) -> Unit)? = null override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ImageHolder { val inflater = LayoutInflater.from(parent.context) @@ -36,7 +40,7 @@ class ImageAdapter(private val context : Context, private val list : List - + xmlns:tools="http://schemas.android.com/tools" + android:layout_width="match_parent" + android:layout_height="match_parent" + tools:viewBindingIgnore="true"> + tools:context=".fragments.ApngDecoderFragment" + tools:viewBindingIgnore="true"> + tools:context=".fragments.JavaFragment" + tools:viewBindingIgnore="true"> + android:layout_height="match_parent" + tools:viewBindingIgnore="true"> + android:orientation="horizontal" + tools:viewBindingIgnore="true">