diff --git a/app/build.gradle.kts b/app/build.gradle.kts index d55dd09..957f70a 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -31,7 +31,6 @@ tasks.register("genDocs") { } } - android { compileSdk = 35 buildToolsVersion = "35.0.0" @@ -84,6 +83,7 @@ dependencies { implementation("io.coil-kt:coil-svg:2.7.0") implementation("androidx.gridlayout:gridlayout:1.0.0") implementation("io.noties.markwon:core:4.6.2") + implementation("com.elvishew:xlog:1.11.1") androidTestImplementation("junit:junit:4.13.2") androidTestImplementation("androidx.test:core:1.6.1") androidTestImplementation("androidx.test.ext:junit:1.2.1") diff --git a/app/src/main/java/com/fredhappyface/ewesticker/ImageKeyboard.kt b/app/src/main/java/com/fredhappyface/ewesticker/ImageKeyboard.kt index 641133b..77ce492 100644 --- a/app/src/main/java/com/fredhappyface/ewesticker/ImageKeyboard.kt +++ b/app/src/main/java/com/fredhappyface/ewesticker/ImageKeyboard.kt @@ -28,6 +28,7 @@ import coil.decode.SvgDecoder import coil.decode.VideoFrameDecoder import coil.imageLoader import coil.load +import com.elvishew.xlog.XLog import com.fredhappyface.ewesticker.adapter.StickerPackAdapter import com.fredhappyface.ewesticker.model.StickerPack import com.fredhappyface.ewesticker.utilities.Cache @@ -94,6 +95,10 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { override fun onCreate() { // Misc super.onCreate() + + XLog.i("=".repeat(80)) + XLog.i("Loaded $packageName:${javaClass.name}") + val scale = baseContext.resources.displayMetrics.density // Setup coil val imageLoader = @@ -113,6 +118,10 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(baseContext) this.backupSharedPreferences = this.getSharedPreferences("backup_prefs", Context.MODE_PRIVATE) + + XLog.i("Loading private shared preferences: ${this.sharedPreferences.all}") + XLog.i("Loading backup shared preferences: ${this.backupSharedPreferences.all}") + this.restoreOnClose = this.backupSharedPreferences.getBoolean("restoreOnClose", false) this.vertical = this.backupSharedPreferences.getBoolean("vertical", false) this.scroll = this.backupSharedPreferences.getBoolean("scroll", false) @@ -147,6 +156,8 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { } this.allStickers += pack.stickerList } + + XLog.i("Loaded all packs: [${this.loadedPacks.keys.joinToString(", ")}]") this.activePack = this.sharedPreferences.getString("activePack", "").toString() // Caches this.sharedPreferences.getString("recentCache", "")?.let { @@ -222,6 +233,7 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { /** When leaving some input field update the caches */ override fun onFinishInput() { + XLog.i("Updating sharedPreferences based on use, and closing...") val editor = this.sharedPreferences.edit() editor.putString("recentCache", this.recentCache.toSharedPref()) editor.putString("compatCache", this.compatCache.toSharedPref()) @@ -240,6 +252,7 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { * @param packName String */ private fun switchPackLayout(packName: String) { + XLog.i("Switching pack to '$packName'") this.activePack = packName for (packCard in this.packsList) { val packButton = packCard.findViewById(R.id.stickerButton) @@ -280,6 +293,7 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { * Set the current tab to the search page/ view */ private fun searchView() { + XLog.i("Switching to search") for (packCard in this.packsList) { val packButton = packCard.findViewById(R.id.stickerButton) if (packButton.tag == "__search__") { @@ -454,9 +468,6 @@ class ImageKeyboard : InputMethodService(), StickerClickListener { this.loadedPacks.keys.sorted() }.toTypedArray() - - - for (sortedPackName in sortedPackNames) { val packButton = addPackButton(sortedPackName) packButton.load(this.loadedPacks[sortedPackName]?.thumbSticker) diff --git a/app/src/main/java/com/fredhappyface/ewesticker/MainActivity.kt b/app/src/main/java/com/fredhappyface/ewesticker/MainActivity.kt index 4d54253..e6457a8 100644 --- a/app/src/main/java/com/fredhappyface/ewesticker/MainActivity.kt +++ b/app/src/main/java/com/fredhappyface/ewesticker/MainActivity.kt @@ -17,12 +17,20 @@ import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.lifecycleScope import androidx.preference.PreferenceManager +import com.elvishew.xlog.LogConfiguration +import com.elvishew.xlog.LogLevel +import com.elvishew.xlog.XLog +import com.elvishew.xlog.printer.AndroidPrinter +import com.elvishew.xlog.printer.file.FilePrinter +import com.elvishew.xlog.printer.file.clean.FileLastModifiedCleanStrategy +import com.fredhappyface.ewesticker.utilities.StickerImporter import com.fredhappyface.ewesticker.utilities.Toaster import com.google.android.material.progressindicator.LinearProgressIndicator import io.noties.markwon.Markwon import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import kotlinx.coroutines.withContext +import java.io.File import java.util.Calendar /** MainActivity class inherits from the AppCompatActivity class - provides the settings view */ @@ -43,6 +51,17 @@ class MainActivity : AppCompatActivity() { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) + val logConfig = LogConfiguration.Builder().logLevel(LogLevel.ALL).tag("EweSticker").build() + val androidPrinter = + AndroidPrinter(true) // Printer that print the log using android.util.Log + val filePrinter = FilePrinter.Builder( + File(filesDir, "logs").path + ).cleanStrategy(FileLastModifiedCleanStrategy(86_400_000)).build() // 1day + XLog.init(logConfig, androidPrinter, filePrinter) + + XLog.i("=".repeat(80)) + XLog.i("Loaded $packageName:$localClassName") + val markwon: Markwon = Markwon.create(this) val featuresText = findViewById(R.id.features_text) markwon.setMarkdown(featuresText, getString(R.string.features_text)) @@ -54,6 +73,9 @@ class MainActivity : AppCompatActivity() { this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this) this.backupSharedPreferences = this.getSharedPreferences("backup_prefs", Context.MODE_PRIVATE) + + XLog.i("Loading private shared preferences: ${this.sharedPreferences.all}") + XLog.i("Loading backup shared preferences: ${this.backupSharedPreferences.all}") this.contextView = findViewById(R.id.activityMainRoot) this.toaster = Toaster(baseContext) refreshStickerDirPath() @@ -71,12 +93,16 @@ class MainActivity : AppCompatActivity() { toggle(findViewById(R.id.insensitive_sort), "insensitiveSort", false) {} val versionText: TextView = findViewById(R.id.versionText) + var version = getString(R.string.version_text) try { val packageInfo = packageManager.getPackageInfo(packageName, 0) - versionText.text = packageInfo.versionName - } catch (e: PackageManager.NameNotFoundException) { - versionText.text = getString(R.string.version_text) + version = packageInfo.versionName ?: version + } catch (_: PackageManager.NameNotFoundException) { } + + versionText.text = version + XLog.i("Version: $version") + } /** @@ -107,6 +133,22 @@ class MainActivity : AppCompatActivity() { } } + private val saveFileLauncher = + registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result -> + if (result.resultCode == RESULT_OK) { + result.data?.data?.also { uri -> + val file = File(filesDir, "logs/log") + if (file.exists()) { + contentResolver.openOutputStream(uri)?.use { outputStream -> + file.inputStream().use { inputStream -> + inputStream.copyTo(outputStream) + } + } + } + } + } + } + /** * Called on button press to launch settings * @@ -129,6 +171,20 @@ class MainActivity : AppCompatActivity() { chooseDirResultLauncher.launch(intent) } + /** + * Called on button press to save logs + * + * @param ignoredView: View + */ + fun saveLogs(ignoredView: View) { + val saveIntent = Intent(Intent.ACTION_CREATE_DOCUMENT).apply { + addCategory(Intent.CATEGORY_OPENABLE) + type = "text/plain" + putExtra(Intent.EXTRA_TITLE, "ewesticker.log") + } + saveFileLauncher.launch(saveIntent) + } + /** * reloadStickers * diff --git a/app/src/main/java/com/fredhappyface/ewesticker/StickerImporter.kt b/app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerImporter.kt similarity index 82% rename from app/src/main/java/com/fredhappyface/ewesticker/StickerImporter.kt rename to app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerImporter.kt index 70e367a..de627e4 100644 --- a/app/src/main/java/com/fredhappyface/ewesticker/StickerImporter.kt +++ b/app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerImporter.kt @@ -1,4 +1,4 @@ -package com.fredhappyface.ewesticker +package com.fredhappyface.ewesticker.utilities import android.content.Context import android.net.Uri @@ -6,8 +6,8 @@ import android.os.Handler import android.os.Looper import android.view.View import androidx.documentfile.provider.DocumentFile -import com.fredhappyface.ewesticker.utilities.Toaster -import com.fredhappyface.ewesticker.utilities.Utils +import com.elvishew.xlog.XLog + import com.google.android.material.progressindicator.LinearProgressIndicator import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.async @@ -28,6 +28,7 @@ private const val BUFFER_SIZE = 64 * 1024 // 64 KB * @property context: application baseContext * @property toaster: an instance of Toaster (used to store an error state for later reporting to the * user) + * @property progressBar: LinearProgressIndicator that we update as we import stickers */ class StickerImporter( private val context: Context, @@ -36,6 +37,7 @@ class StickerImporter( ) { private val supportedMimes = Utils.getSupportedMimes() private val packSizes: MutableMap = mutableMapOf() + private var detectedStickers = 0 private var totalStickers = 0 private val mainHandler = Handler(Looper.getMainLooper()) @@ -53,14 +55,17 @@ class StickerImporter( * @param stickerDirPath a URI to the stickers directory to import into EweSticker */ suspend fun importStickers(stickerDirPath: String): Int { + XLog.i("Removing old stickers...") File(context.filesDir, "stickers").deleteRecursively() withContext(Dispatchers.Main) { progressBar.visibility = View.VISIBLE progressBar.isIndeterminate = true } + XLog.i("Walking $stickerDirPath...") val leafNodes = fileWalk(DocumentFile.fromTreeUri(context, Uri.parse(stickerDirPath))) if (leafNodes.size > MAX_FILES) { + XLog.w("Found more than $MAX_FILES stickers, notify user") toaster.setState(1) } @@ -69,6 +74,7 @@ class StickerImporter( } // Perform concurrent file copy operations + XLog.i("Perform concurrent file copy operations...") withContext(Dispatchers.IO) { leafNodes.take(MAX_FILES).mapIndexed { index, file -> async { @@ -84,7 +90,9 @@ class StickerImporter( progressBar.visibility = View.GONE } - return leafNodes.size + XLog.i("Copied $totalStickers / $detectedStickers") + + return totalStickers } /** @@ -98,10 +106,12 @@ class StickerImporter( val parentDir = sticker.parentFile?.name ?: "__default__" val packSize = packSizes[parentDir] ?: 0 if (packSize > MAX_PACK_SIZE) { + XLog.w("Found more than $MAX_PACK_SIZE stickers in '$parentDir', notify user") toaster.setState(2) return } if (sticker.type !in supportedMimes) { + XLog.w("'$parentDir/${sticker.name}' is not a supported mimetype (${sticker.type}), notify user") toaster.setState(3) return } @@ -126,7 +136,9 @@ class StickerImporter( } totalStickers++ } - } catch (_: IOException) { + } catch (e: IOException) { + XLog.e("There was an IOException when copying '${parentDir}/${sticker.name}'!") + XLog.e(e) } } @@ -148,9 +160,10 @@ class StickerImporter( currentFile?.listFiles()?.forEach { file -> if (file.isFile) { leafNodes.add(file) - totalStickers++ + detectedStickers++ - if (leafNodes.size >= MAX_FILES) { + if (leafNodes.size > MAX_FILES + 1) { + XLog.w("Found more than ${MAX_FILES + 1} stickers, so returning early") return leafNodes } } else if (file.isDirectory) { diff --git a/app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerSender.kt b/app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerSender.kt index ce58f59..e5558db 100644 --- a/app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerSender.kt +++ b/app/src/main/java/com/fredhappyface/ewesticker/utilities/StickerSender.kt @@ -4,7 +4,6 @@ import android.content.ClipDescription import android.content.Context import android.content.Intent import android.graphics.Bitmap -import android.util.Log import android.view.inputmethod.EditorInfo import android.view.inputmethod.InputConnection import androidx.core.content.FileProvider @@ -13,6 +12,7 @@ import androidx.core.view.inputmethod.InputConnectionCompat import androidx.core.view.inputmethod.InputContentInfoCompat import coil.ImageLoader import coil.request.ImageRequest +import com.elvishew.xlog.XLog import com.fredhappyface.ewesticker.R import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers @@ -50,6 +50,15 @@ class StickerSender( private val supportedMimes = this.currentInputEditorInfo?.contentMimeTypes ?: emptyArray() private val packageName = this.currentInputEditorInfo?.packageName + init { + XLog.i("Connecting to $packageName which supports [${supportedMimes.joinToString(", ")}]") + } + + /** + * Wrapper function to display a toast message to the user + * + * @param message String + */ private fun showToast(message: String) { CoroutineScope(Dispatchers.Main).launch { toaster.toast(message) @@ -68,6 +77,8 @@ class StickerSender( val compatSticker = File(internalDir, "__compatSticker__/$compatStickerName.png") if (!compatSticker.exists()) { + XLog.i("Create a fallback png sticker '__compatSticker__/$compatStickerName.png'") + compatSticker.parentFile?.mkdirs() try { val request = ImageRequest.Builder(context) @@ -95,6 +106,11 @@ class StickerSender( return compatSticker } + /** + * Main method to send a sticker to an InputConnectionCompat, this attempts to send via the happy path (assuming the + * InputConnectionCompat supports the stickers mime type. Otherwise attempts falling back to png, and then finally if that fails, + * opening a share sheet + */ fun sendSticker(file: File) { val stickerType = Utils.getMimeType(file) ?: "__unknown__" @@ -105,63 +121,28 @@ class StickerSender( || "video/*" in supportedMimes && stickerType.startsWith("video/")) && stickerType != "image/svg+xml" ) { - if (!doCommitContent(stickerType, file)) { CoroutineScope(Dispatchers.Main).launch { - doFallbackCommitContent(file) + doFallbackCommitContent(stickerType, file) } } } else { CoroutineScope(Dispatchers.Main).launch { - doFallbackCommitContent(file) + doFallbackCommitContent(stickerType, file) } } } - private fun openShareSheet(file: File) { - val uri = FileProvider.getUriForFile( - context, - "com.fredhappyface.ewesticker.inputcontent", - file, - ) - - val shareIntent = Intent().apply { - action = Intent.ACTION_SEND - putExtra(Intent.EXTRA_STREAM, uri) - type = "image/*" - } - - shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - - val chooserIntent = Intent.createChooser(shareIntent, "Share Sticker") - chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - context.startActivity(chooserIntent) - } - - private suspend fun doFallbackCommitContent(file: File) { - - if ("image/png" in supportedMimes || "image/*" in supportedMimes) { - val compatSticker = createCompatSticker(file) - if (compatSticker != null) { - if (!doCommitContent("image/png", compatSticker)) { - openShareSheet(file) - } - return - } - } - openShareSheet(file) - - } - /** - * Send a sticker file to a InputConnectionCompat + * Called by sendSticker. Send a sticker file to a InputConnectionCompat * * @param mimeType String * @param file File + * @return success Boolean */ private fun doCommitContent(mimeType: String, file: File): Boolean { -// Log.d("QWERTY", "Sending ${file.name} ($mimeType) to ${this.packageName}") + XLog.i("Sending ${file.name} ($mimeType) to ${this.packageName}") val inputContentInfoCompat = InputContentInfoCompat( FileProvider.getUriForFile( context, @@ -185,4 +166,53 @@ class StickerSender( return false } + /** + * Called by sendSticker. Otherwise attempts falling back to png, and then finally if that fails, opening a + * share sheet to send the sticker + * + * @param mimeType String + * @param file File + */ + private suspend fun doFallbackCommitContent(mimeType: String, file: File) { + + if ("image/png" in supportedMimes || "image/*" in supportedMimes) { + val compatSticker = createCompatSticker(file) + if (compatSticker != null) { + if (!doCommitContent("image/png", compatSticker)) { + openShareSheet(mimeType, file) + } + return + } + } + openShareSheet(mimeType, file) + + } + + /** + * Called by doFallbackCommitContent. Opens a share sheet to send the sticker + * + * @param mimeType String + * @param file File + */ + private fun openShareSheet(mimeType: String, file: File) { + XLog.i("$packageName reports that is doesn't support png over its InputConnectionCompat, so open a share sheet") + val uri = FileProvider.getUriForFile( + context, + "com.fredhappyface.ewesticker.inputcontent", + file, + ) + + val shareIntent = Intent().apply { + action = Intent.ACTION_SEND + putExtra(Intent.EXTRA_STREAM, uri) + type = mimeType + } + + shareIntent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + + val chooserIntent = Intent.createChooser(shareIntent, "Share Sticker") + chooserIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + context.startActivity(chooserIntent) + } + } diff --git a/app/src/main/res/layout/activity_main.xml b/app/src/main/res/layout/activity_main.xml index baaabca..9136748 100644 --- a/app/src/main/res/layout/activity_main.xml +++ b/app/src/main/res/layout/activity_main.xml @@ -405,6 +405,25 @@ android:text="@string/version_text" /> + + + + + + + +