add help docs, refactor import and sending functionality
This commit is contained in:
parent
22f1b38da1
commit
4276c16a98
@ -10,6 +10,8 @@ patch-level version changes can be found in [commit messages](../../commits/mast
|
||||
## Next_Ver - 2022/xx/xx
|
||||
|
||||
- New Feature: Improve error messages for the user per https://github.com/FredHappyface/Android.EweSticker/issues/39
|
||||
- New Feature: Add help (HELP.md) to provide guidance for error messages
|
||||
- Bugfix: Refactor in response bug found when investigating https://github.com/FredHappyface/Android.EweSticker/issues/37
|
||||
- Bugfix: Back button now enabled in fresh install per https://github.com/FredHappyface/Android.EweSticker/issues/38
|
||||
- Update navbar theme (dark/light rather than the app accent colour)
|
||||
- Update dependencies
|
||||
|
95
HELP.md
Normal file
95
HELP.md
Normal file
@ -0,0 +1,95 @@
|
||||
|
||||
<!-- omit in toc -->
|
||||
# Help Guide
|
||||
|
||||
Support is provided via GitHub issues, please note this is provided on a voluntary basis
|
||||
|
||||
Please take a look at [Error Codes](#error-codes) first. This may provide some useful information
|
||||
for why you are getting a certain error code. If an issue is created that is answered by this section
|
||||
you'll likely be asked if you've read this :)
|
||||
|
||||
- [Error Codes](#error-codes)
|
||||
- [E031](#e031)
|
||||
- [E032](#e032)
|
||||
- [E033](#e033)
|
||||
- [E040](#e040)
|
||||
- [E041](#e041)
|
||||
- [E050](#e050)
|
||||
- [Reach out](#reach-out)
|
||||
|
||||
## Error Codes
|
||||
|
||||
### E031
|
||||
Some stickers failed to import (some number imported). Max stickers reached
|
||||
|
||||
This means that the total number of stickers that you are trying to import exceeds the
|
||||
maximum number of stickers supported by EweSticker. Try and import fewer stickers,
|
||||
see [Tutorial](/TUTORIAL.md)
|
||||
|
||||
**NOTE:** that the maximum pack size is currently **128** and the total maximum number of stickers supported
|
||||
is **4096**
|
||||
|
||||
If you feel strongly that the maximum limit should be increased, open an issue (use the
|
||||
'Question' template) and make a request - Be sure to explain why this would be useful. Simply saying
|
||||
'I want 20000 stickers!' will likely result in the issue being closed
|
||||
|
||||
### E032
|
||||
Some stickers failed to import (some number imported). Max pack size reached
|
||||
|
||||
This means that one of your sticker packs contains a number of stickers that exceeds the
|
||||
maximum pack size supported by EweSticker. Try splitting the pack up into smaller chunks,
|
||||
see [Tutorial](/TUTORIAL.md)
|
||||
|
||||
**NOTE:** that the maximum pack size is currently **128** and the total maximum number of stickers supported
|
||||
is **4096**
|
||||
|
||||
If you feel strongly that the maximum limit should be increased, open an issue (use the
|
||||
'Question' template) and make a request - Be sure to explain why this would be useful. Simply saying
|
||||
'I want 20000 stickers!' will likely result in the issue being closed
|
||||
|
||||
### E033
|
||||
Some stickers failed to import (some number imported). Unsupported formats found
|
||||
|
||||
This could be for a few reasons, perhaps you have a non sticker file in the sticker directory such
|
||||
as a document in the wrong place. Alternatively this may result in a seemingly valid sticker not being
|
||||
imported. Chances are that the sticker is not in a [supported format](/README.md#features).
|
||||
|
||||
### E040
|
||||
(image type) not supported here
|
||||
|
||||
The application you are using doesn't support a sticker format or the compat-format
|
||||
|
||||
Unfortunately, nothing can be done by EweSticker to solve this, you may need to contact the application
|
||||
developer you are trying to send a sticker to
|
||||
|
||||
### E041
|
||||
Unexpected IOException when converting sticker
|
||||
|
||||
This is an unexpected error and happened when creating a compat-sticker to send to the application.
|
||||
Please open an issue and provide as much information as you can. E.g. Android Version, phone
|
||||
manufacturer, app you are trying to send the sticker in
|
||||
|
||||
### E050
|
||||
IllegalStateException when switching packs. Try switching away from and back to EweSticker
|
||||
|
||||
This sometimes happens if you leave EweSticker as the current keyboard and switch back to it. The best
|
||||
way to solve this is to tap the back button in the pack selector and switch back to EweSticker.
|
||||
Please open an issue and provide as much information as you can. E.g. Android Version, phone
|
||||
manufacturer, app you are trying to send the sticker in.
|
||||
|
||||
## Reach out
|
||||
|
||||
Support is provided via GitHub issues, please note this is provided on a voluntary basis
|
||||
|
||||
you are therefore not entitled to free customer service (that is not to say that contributions/ issues and questions are not welcome - more reminding you that project maintainers are well within their rights to prioritize other issues).
|
||||
|
||||
https://github.com/FredHappyface/.github/blob/master/SUPPORT.md provides a little more info
|
||||
from the types of support you can expect
|
||||
|
||||
Please make sure to read https://github.com/FredHappyface/Android.EweSticker/issues/21 before
|
||||
opening an issue, this ay seem a bit grumpy but chances are I won't be able to help with your
|
||||
issue if you do not fill in the template provided
|
||||
|
||||
To open a new issue click the following link: https://github.com/FredHappyface/Android.EweSticker/issues/new/choose
|
||||
|
||||
**NOTE:** you will need to have a GitHub account to open issues (create one at https://github.com/signup)
|
@ -16,6 +16,7 @@ Sticker-board for android inspired by uSticker (forked from woosticker).
|
||||
- [Features](#features)
|
||||
- [Screenshots](#screenshots)
|
||||
- [How to use](#how-to-use)
|
||||
- [Help](#help)
|
||||
- [Lint with](#lint-with)
|
||||
- [Language Information](#language-information)
|
||||
- [Kotlin and Android Version](#kotlin-and-android-version)
|
||||
@ -72,6 +73,10 @@ Sticker-board for android inspired by uSticker (forked from woosticker).
|
||||
|
||||
See the [Tutorial](/TUTORIAL.md) for more information.
|
||||
|
||||
## Help
|
||||
|
||||
See the [Help](/HELP.md) for more information.
|
||||
|
||||
## Lint with
|
||||
|
||||
```txt
|
||||
|
14
TUTORIAL.md
14
TUTORIAL.md
@ -1,9 +1,18 @@
|
||||
|
||||
<!-- omit in toc -->
|
||||
# Tutorial
|
||||
|
||||
See below for a step-by-step tutorial on how to use EweSticker with your existing
|
||||
sticker collection.
|
||||
|
||||
- [Step 1 - Create Sticker Directory (and transfer to device)](#step-1---create-sticker-directory-and-transfer-to-device)
|
||||
- [Step 2 - Download EweSticker](#step-2---download-ewesticker)
|
||||
- [Get it on F-Droid](#get-it-on-f-droid)
|
||||
- [Get it on Google Play](#get-it-on-google-play)
|
||||
- [Download the APK](#download-the-apk)
|
||||
- [Step 3 - Select Directory with EweSticker (and wait...)](#step-3---select-directory-with-ewesticker-and-wait)
|
||||
- [Step 4 - Activate the keyboard](#step-4---activate-the-keyboard)
|
||||
- [Step 5 - Send Stickers in your favourite apps](#step-5---send-stickers-in-your-favourite-apps)
|
||||
|
||||
## Step 1 - Create Sticker Directory (and transfer to device)
|
||||
|
||||
<img src="readme-assets/tutorial/step1.png" alt="Step 1" width="600">
|
||||
@ -23,6 +32,9 @@ The sticker directory has the following structure:
|
||||
Then transfer this to your phone/ device. Plugging this into a PC is a pretty
|
||||
convenient way to do this.
|
||||
|
||||
**NOTE:** that the maximum pack size is currently **128** and the total maximum number of stickers supported
|
||||
is **4096**
|
||||
|
||||
## Step 2 - Download EweSticker
|
||||
|
||||
### Get it on F-Droid
|
||||
|
@ -8,7 +8,7 @@
|
||||
android:label="@string/app_name"
|
||||
android:theme="@style/AppTheme"
|
||||
tools:ignore="GoogleAppIndexingWarning"
|
||||
android:dataExtractionRules="@xml/data_extraction_rules">
|
||||
android:dataExtractionRules="@xml/data_extraction_rules" tools:targetApi="s">
|
||||
<activity
|
||||
android:name="com.fredhappyface.ewesticker.MainActivity"
|
||||
android:configChanges="keyboard|keyboardHidden|orientation|screenSize"
|
||||
|
@ -1,8 +1,6 @@
|
||||
package com.fredhappyface.ewesticker
|
||||
|
||||
import android.content.ClipDescription
|
||||
import android.content.SharedPreferences
|
||||
import android.graphics.Bitmap
|
||||
import android.inputmethodservice.InputMethodService
|
||||
import android.os.Build.VERSION.SDK_INT
|
||||
import android.view.View
|
||||
@ -12,11 +10,6 @@ import android.view.inputmethod.InputMethodManager
|
||||
import android.widget.FrameLayout
|
||||
import android.widget.ImageButton
|
||||
import android.widget.RelativeLayout
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||
import androidx.core.view.iterator
|
||||
import androidx.gridlayout.widget.GridLayout
|
||||
import androidx.preference.PreferenceManager
|
||||
@ -27,13 +20,8 @@ import coil.decode.ImageDecoderDecoder
|
||||
import coil.fetch.VideoFrameFileFetcher
|
||||
import coil.imageLoader
|
||||
import coil.load
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.*
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
import java.util.*
|
||||
import kotlin.collections.HashMap
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
@ -62,7 +50,7 @@ class ImageKeyboard : InputMethodService() {
|
||||
private var recentCache = Cache()
|
||||
|
||||
// onStartInput
|
||||
private lateinit var supportedMimes: List<String>
|
||||
private lateinit var stickerSender: StickerSender
|
||||
|
||||
// onCreateInputView
|
||||
private lateinit var keyboardRoot: ViewGroup
|
||||
@ -83,7 +71,7 @@ class ImageKeyboard : InputMethodService() {
|
||||
override fun onCreate() {
|
||||
// Misc
|
||||
super.onCreate()
|
||||
val scale = applicationContext.resources.displayMetrics.density
|
||||
val scale = baseContext.resources.displayMetrics.density
|
||||
// Setup coil
|
||||
val imageLoader =
|
||||
ImageLoader.Builder(baseContext)
|
||||
@ -112,7 +100,7 @@ class ImageKeyboard : InputMethodService() {
|
||||
(this.sharedPreferences.getInt("iconSize", 80) * scale)
|
||||
})
|
||||
.toInt()
|
||||
this.toaster = Toaster(applicationContext)
|
||||
this.toaster = Toaster(baseContext)
|
||||
// Load Packs
|
||||
this.loadedPacks = HashMap()
|
||||
val packs =
|
||||
@ -145,7 +133,7 @@ class ImageKeyboard : InputMethodService() {
|
||||
* @return View keyboardLayout
|
||||
*/
|
||||
override fun onCreateInputView(): View {
|
||||
val keyboardLayout = View.inflate(applicationContext, R.layout.keyboard_layout, null)
|
||||
val keyboardLayout = View.inflate(baseContext, R.layout.keyboard_layout, null)
|
||||
this.keyboardRoot = keyboardLayout.findViewById(R.id.keyboardRoot)
|
||||
this.packsList = keyboardLayout.findViewById(R.id.packsList)
|
||||
this.packContent = keyboardLayout.findViewById(R.id.packContent)
|
||||
@ -183,8 +171,15 @@ class ImageKeyboard : InputMethodService() {
|
||||
* @param restarting
|
||||
*/
|
||||
override fun onStartInput(info: EditorInfo?, restarting: Boolean) {
|
||||
this.supportedMimes =
|
||||
Utils.getSupportedMimes().filter { isCommitContentSupported(info, it) }
|
||||
this.stickerSender = StickerSender(
|
||||
this.baseContext,
|
||||
this.toaster,
|
||||
this.internalDir,
|
||||
this.currentInputConnection,
|
||||
this.currentInputEditorInfo,
|
||||
this.compatCache,
|
||||
this.imageLoader
|
||||
)
|
||||
}
|
||||
|
||||
/** When leaving some input field update the caches */
|
||||
@ -197,91 +192,6 @@ class ImageKeyboard : InputMethodService() {
|
||||
super.onFinishInput()
|
||||
}
|
||||
|
||||
/**
|
||||
* In the event that a mimetype is unsupported by a InputConnectionCompat (looking at you,
|
||||
* Signal) create a temporary png and send that. In the event that png is not supported, alert
|
||||
* the user.
|
||||
*
|
||||
* @param file: File
|
||||
*/
|
||||
private suspend fun doFallbackCommitContent(file: File) {
|
||||
// PNG might not be supported
|
||||
if ("image/png" !in this.supportedMimes) {
|
||||
toaster.toast(getString(R.string.fallback_040, file.extension))
|
||||
return
|
||||
}
|
||||
// Create a new compatSticker and convert the sticker to png
|
||||
val compatStickerName = file.hashCode().toString()
|
||||
val compatSticker = File(this.internalDir, "__compatSticker__/$compatStickerName.png")
|
||||
if (!compatSticker.exists()) {
|
||||
// If the sticker doesn't exist then create
|
||||
compatSticker.parentFile?.mkdirs()
|
||||
try {
|
||||
val request =
|
||||
ImageRequest.Builder(baseContext)
|
||||
.data(file)
|
||||
.target { result ->
|
||||
val bitmap = result.toBitmap()
|
||||
bitmap.compress(
|
||||
Bitmap.CompressFormat.PNG, 90, FileOutputStream(compatSticker)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
imageLoader.execute(request)
|
||||
} catch (ignore: IOException) {
|
||||
toaster.toast(getString(R.string.fallback_041))
|
||||
}
|
||||
}
|
||||
// Send the compatSticker!
|
||||
doCommitContent("image/png", compatSticker)
|
||||
// Remove old stickers
|
||||
val remSticker = this.compatCache.add(compatStickerName)
|
||||
remSticker?.let { File(this.internalDir, "__compatSticker__/$remSticker.png").delete() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a sticker file to a InputConnectionCompat
|
||||
*
|
||||
* @param mimeType String
|
||||
* @param file File
|
||||
*/
|
||||
private fun doCommitContent(mimeType: String, file: File) {
|
||||
// ContentUri, ClipDescription, linkUri
|
||||
val inputContentInfoCompat =
|
||||
InputContentInfoCompat(
|
||||
FileProvider.getUriForFile(this, "com.fredhappyface.ewesticker.inputcontent", file),
|
||||
ClipDescription(file.name, arrayOf(mimeType)),
|
||||
null
|
||||
)
|
||||
// InputConnection, EditorInfo, InputContentInfoCompat, int flags, null opts
|
||||
InputConnectionCompat.commitContent(
|
||||
currentInputConnection,
|
||||
currentInputEditorInfo,
|
||||
inputContentInfoCompat,
|
||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sticker is supported by the receiver
|
||||
*
|
||||
* @param editorInfo: EditorInfo - the editor/ receiver
|
||||
* @param mimeType: String - the image mimetype
|
||||
* @return boolean - is the mimetype supported?
|
||||
*/
|
||||
private fun isCommitContentSupported(editorInfo: EditorInfo?, mimeType: String?): Boolean {
|
||||
editorInfo?.packageName ?: return false
|
||||
mimeType ?: return false
|
||||
currentInputConnection ?: return false
|
||||
EditorInfoCompat.getContentMimeTypes(editorInfo).forEach {
|
||||
if (ClipDescription.compareMimeTypes(mimeType, it)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap the pack layout every time a pack is selected. If already cached use that otherwise
|
||||
* create the pack layout
|
||||
@ -362,12 +272,7 @@ class ImageKeyboard : InputMethodService() {
|
||||
imgButton.setOnClickListener {
|
||||
val file = it.tag as File
|
||||
this.recentCache.add(file.absolutePath)
|
||||
val stickerType = Utils.getMimeType(file)
|
||||
if (stickerType == null || stickerType !in this.supportedMimes) {
|
||||
CoroutineScope(Dispatchers.Main).launch { doFallbackCommitContent(file) }
|
||||
return@setOnClickListener
|
||||
}
|
||||
doCommitContent(stickerType, file)
|
||||
stickerSender.sendSticker(file)
|
||||
}
|
||||
imgButton.setOnLongClickListener { view: View ->
|
||||
val file = view.tag as File
|
||||
@ -415,7 +320,7 @@ class ImageKeyboard : InputMethodService() {
|
||||
if (SDK_INT >= 28) {
|
||||
this.switchToPreviousInputMethod()
|
||||
} else {
|
||||
(applicationContext.getSystemService(INPUT_METHOD_SERVICE) as
|
||||
(baseContext.getSystemService(INPUT_METHOD_SERVICE) as
|
||||
InputMethodManager)
|
||||
.showInputMethodPicker()
|
||||
}
|
||||
|
@ -3,7 +3,6 @@ package com.fredhappyface.ewesticker
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import android.content.SharedPreferences
|
||||
import android.net.Uri
|
||||
import android.os.Bundle
|
||||
import android.os.Handler
|
||||
import android.os.Looper
|
||||
@ -16,31 +15,18 @@ import android.widget.SeekBar.OnSeekBarChangeListener
|
||||
import android.widget.TextView
|
||||
import androidx.activity.result.contract.ActivityResultContracts
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import androidx.preference.PreferenceManager
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
import java.util.*
|
||||
import java.util.concurrent.Executors
|
||||
|
||||
private const val MAX_FILES = 4096
|
||||
private const val MAX_PACK_SIZE = 128
|
||||
|
||||
/** MainActivity class inherits from the AppCompatActivity class - provides the settings view */
|
||||
class MainActivity : AppCompatActivity() {
|
||||
// init
|
||||
private val supportedMimes = Utils.getSupportedMimes()
|
||||
|
||||
// onCreate
|
||||
private lateinit var sharedPreferences: SharedPreferences
|
||||
private lateinit var contextView: View
|
||||
private lateinit var toaster: Toaster
|
||||
|
||||
// importSticker(s)
|
||||
private var filesLeft = MAX_FILES
|
||||
private var packSizes: MutableMap<String, Int> = mutableMapOf()
|
||||
private var totalStickers = 0
|
||||
|
||||
/**
|
||||
* Sets up content view, shared prefs, etc.
|
||||
*
|
||||
@ -53,7 +39,7 @@ class MainActivity : AppCompatActivity() {
|
||||
// Set late-init attrs
|
||||
this.sharedPreferences = PreferenceManager.getDefaultSharedPreferences(this)
|
||||
this.contextView = findViewById(R.id.activityMainRoot)
|
||||
this.toaster = Toaster(applicationContext)
|
||||
this.toaster = Toaster(baseContext)
|
||||
refreshStickerDirPath()
|
||||
// Update UI with config
|
||||
seekBar(findViewById(R.id.iconsPerXSb), findViewById(R.id.iconsPerXLbl), "iconsPerX", 3)
|
||||
@ -72,13 +58,14 @@ class MainActivity : AppCompatActivity() {
|
||||
registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
|
||||
if (result.resultCode == Activity.RESULT_OK) {
|
||||
val editor = this.sharedPreferences.edit()
|
||||
editor.putString("stickerDirPath", result.data?.data.toString())
|
||||
val stickerDirPath = result.data?.data.toString()
|
||||
editor.putString("stickerDirPath", stickerDirPath)
|
||||
editor.putString("lastUpdateDate", Calendar.getInstance().time.toString())
|
||||
editor.putString("recentCache", "")
|
||||
editor.putString("compatCache", "")
|
||||
editor.apply()
|
||||
refreshStickerDirPath()
|
||||
importStickers()
|
||||
importStickers(stickerDirPath)
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,46 +85,14 @@ class MainActivity : AppCompatActivity() {
|
||||
* @param view: View
|
||||
*/
|
||||
fun chooseDir(view: View) {
|
||||
this.filesLeft = MAX_FILES
|
||||
this.totalStickers = 0
|
||||
val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE)
|
||||
intent.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
intent.addFlags(Intent.FLAG_GRANT_PERSISTABLE_URI_PERMISSION)
|
||||
chooseDirResultLauncher.launch(intent)
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies stickers from source to internal storage
|
||||
*
|
||||
* @param sticker sticker to copy over
|
||||
*
|
||||
* @return 1 if sticker imported successfully else 0
|
||||
*/
|
||||
private fun importSticker(sticker: DocumentFile) {
|
||||
// Exit if sticker is unsupported or if pack size > MAX_PACK_SIZE
|
||||
val parentDir = sticker.parentFile?.name ?: "__default__"
|
||||
val packSize = this.packSizes[parentDir] ?: 0
|
||||
if (packSize > MAX_PACK_SIZE) {
|
||||
this.toaster.setState(2); return
|
||||
}
|
||||
if (sticker.type !in this.supportedMimes) {
|
||||
this.toaster.setState(3); return
|
||||
}
|
||||
this.packSizes[parentDir] = packSize + 1
|
||||
// Copy sticker to app storage
|
||||
val destSticker = File(filesDir, "stickers/${parentDir}/${sticker.name}")
|
||||
destSticker.parentFile?.mkdirs()
|
||||
try {
|
||||
val inputStream = contentResolver.openInputStream(sticker.uri)
|
||||
Files.copy(inputStream, destSticker.toPath())
|
||||
inputStream?.close()
|
||||
} catch (e: java.lang.Exception) {
|
||||
}
|
||||
this.totalStickers++
|
||||
}
|
||||
|
||||
/** Import files from storage to internal directory */
|
||||
private fun importStickers() {
|
||||
private fun importStickers(stickerDirPath: String) {
|
||||
// Use worker thread because this takes several seconds
|
||||
val executor = Executors.newSingleThreadExecutor()
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
@ -147,30 +102,21 @@ class MainActivity : AppCompatActivity() {
|
||||
val button = findViewById<Button>(R.id.updateStickerPackInfoBtn)
|
||||
button.isEnabled = false
|
||||
executor.execute {
|
||||
File(filesDir, "stickers").deleteRecursively()
|
||||
val stickerDirPath =
|
||||
this.sharedPreferences.getString(
|
||||
"stickerDirPath", resources.getString(R.string.update_sticker_pack_info_path)
|
||||
val totalStickers =
|
||||
StickerImporter(baseContext, this.toaster).importStickers(
|
||||
stickerDirPath
|
||||
)
|
||||
val leafNodes =
|
||||
fileWalk(DocumentFile.fromTreeUri(applicationContext, Uri.parse(stickerDirPath)))
|
||||
if (leafNodes.size > MAX_FILES) {
|
||||
toaster.setState(1)
|
||||
}
|
||||
for (file in leafNodes.take(MAX_FILES)) {
|
||||
importSticker(file)
|
||||
}
|
||||
handler.post {
|
||||
toaster.toastOnState(
|
||||
arrayOf(
|
||||
getString(R.string.imported_020, this.totalStickers),
|
||||
getString(R.string.imported_031, this.totalStickers),
|
||||
getString(R.string.imported_032, this.totalStickers),
|
||||
getString(R.string.imported_033, this.totalStickers),
|
||||
getString(R.string.imported_020, totalStickers),
|
||||
getString(R.string.imported_031, totalStickers),
|
||||
getString(R.string.imported_032, totalStickers),
|
||||
getString(R.string.imported_033, totalStickers),
|
||||
)
|
||||
)
|
||||
val editor = this.sharedPreferences.edit()
|
||||
editor.putInt("numStickersImported", this.totalStickers)
|
||||
editor.putInt("numStickersImported", totalStickers)
|
||||
editor.apply()
|
||||
refreshStickerDirPath()
|
||||
button.isEnabled = true
|
||||
@ -178,25 +124,6 @@ class MainActivity : AppCompatActivity() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MutableSet of DocumentFiles from a root node
|
||||
*
|
||||
* @param rootNode parent dir to get all files from
|
||||
* @return MutableSet<DocumentFile> set of files
|
||||
*/
|
||||
private fun fileWalk(rootNode: DocumentFile?): MutableSet<DocumentFile> {
|
||||
val leafNodes = mutableSetOf<DocumentFile>()
|
||||
if (rootNode == null || this.filesLeft < 0) {
|
||||
return leafNodes
|
||||
}
|
||||
val files = rootNode.listFiles()
|
||||
for (file in files) {
|
||||
if (file.isFile) leafNodes.add(file)
|
||||
if (file.isDirectory) leafNodes.addAll(fileWalk(file))
|
||||
}
|
||||
this.filesLeft -= files.size
|
||||
return leafNodes
|
||||
}
|
||||
|
||||
/**
|
||||
* Add toggle logic for each toggle/ checkbox in the layout
|
||||
|
@ -0,0 +1,94 @@
|
||||
package com.fredhappyface.ewesticker
|
||||
|
||||
import android.content.Context
|
||||
import android.net.Uri
|
||||
import androidx.documentfile.provider.DocumentFile
|
||||
import java.io.File
|
||||
import java.nio.file.Files
|
||||
|
||||
private const val MAX_FILES = 4096
|
||||
private const val MAX_PACK_SIZE = 128
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @property context
|
||||
* @property toaster
|
||||
*/
|
||||
class StickerImporter(
|
||||
private val context: Context,
|
||||
private val toaster: Toaster,
|
||||
) {
|
||||
private val supportedMimes = Utils.getSupportedMimes()
|
||||
private var filesLeft = MAX_FILES
|
||||
private var packSizes: MutableMap<String, Int> = mutableMapOf()
|
||||
private var totalStickers = 0
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param stickerDirPath
|
||||
*/
|
||||
fun importStickers(stickerDirPath: String): Int {
|
||||
File(this.context.filesDir, "stickers").deleteRecursively()
|
||||
val leafNodes =
|
||||
fileWalk(DocumentFile.fromTreeUri(context, Uri.parse(stickerDirPath)))
|
||||
if (leafNodes.size > MAX_FILES) {
|
||||
this.toaster.setState(1)
|
||||
}
|
||||
for (file in leafNodes.take(MAX_FILES)) {
|
||||
importSticker(file)
|
||||
}
|
||||
return this.totalStickers
|
||||
}
|
||||
|
||||
/**
|
||||
* Copies stickers from source to internal storage
|
||||
*
|
||||
* @param sticker sticker to copy over
|
||||
*
|
||||
* @return 1 if sticker imported successfully else 0
|
||||
*/
|
||||
private fun importSticker(sticker: DocumentFile) {
|
||||
// Exit if sticker is unsupported or if pack size > MAX_PACK_SIZE
|
||||
val parentDir = sticker.parentFile?.name ?: "__default__"
|
||||
val packSize = this.packSizes[parentDir] ?: 0
|
||||
if (packSize > MAX_PACK_SIZE) {
|
||||
this.toaster.setState(2); return
|
||||
}
|
||||
if (sticker.type !in this.supportedMimes) {
|
||||
this.toaster.setState(3); return
|
||||
}
|
||||
this.packSizes[parentDir] = packSize + 1
|
||||
// Copy sticker to app storage
|
||||
val destSticker = File(this.context.filesDir, "stickers/${parentDir}/${sticker.name}")
|
||||
destSticker.parentFile?.mkdirs()
|
||||
try {
|
||||
val inputStream = context.contentResolver.openInputStream(sticker.uri)
|
||||
Files.copy(inputStream, destSticker.toPath())
|
||||
inputStream?.close()
|
||||
} catch (e: java.lang.Exception) {
|
||||
}
|
||||
this.totalStickers++
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a MutableSet of DocumentFiles from a root node
|
||||
*
|
||||
* @param rootNode parent dir to get all files from
|
||||
* @return MutableSet<DocumentFile> set of files
|
||||
*/
|
||||
private fun fileWalk(rootNode: DocumentFile?): MutableSet<DocumentFile> {
|
||||
val leafNodes = mutableSetOf<DocumentFile>()
|
||||
if (rootNode == null || this.filesLeft < 0) {
|
||||
return leafNodes
|
||||
}
|
||||
val files = rootNode.listFiles()
|
||||
for (file in files) {
|
||||
if (file.isFile) leafNodes.add(file)
|
||||
if (file.isDirectory) leafNodes.addAll(fileWalk(file))
|
||||
}
|
||||
this.filesLeft -= files.size
|
||||
return leafNodes
|
||||
}
|
||||
}
|
151
app/src/main/java/com/fredhappyface/ewesticker/StickerSender.kt
Normal file
151
app/src/main/java/com/fredhappyface/ewesticker/StickerSender.kt
Normal file
@ -0,0 +1,151 @@
|
||||
package com.fredhappyface.ewesticker
|
||||
|
||||
import android.content.ClipDescription
|
||||
import android.content.Context
|
||||
import android.graphics.Bitmap
|
||||
import android.view.inputmethod.EditorInfo
|
||||
import android.view.inputmethod.InputConnection
|
||||
import androidx.core.content.FileProvider
|
||||
import androidx.core.graphics.drawable.toBitmap
|
||||
import androidx.core.view.inputmethod.EditorInfoCompat
|
||||
import androidx.core.view.inputmethod.InputConnectionCompat
|
||||
import androidx.core.view.inputmethod.InputContentInfoCompat
|
||||
import coil.ImageLoader
|
||||
import coil.request.ImageRequest
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.io.FileOutputStream
|
||||
import java.io.IOException
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @property context
|
||||
* @property toaster
|
||||
* @property internalDir
|
||||
* @property currentInputConnection
|
||||
* @property currentInputEditorInfo
|
||||
* @property compatCache
|
||||
* @property imageLoader
|
||||
*/
|
||||
class StickerSender(
|
||||
private val context: Context,
|
||||
private val toaster: Toaster,
|
||||
private val internalDir: File,
|
||||
private val currentInputConnection: InputConnection?,
|
||||
private val currentInputEditorInfo: EditorInfo?,
|
||||
private val compatCache: Cache,
|
||||
private val imageLoader: ImageLoader,
|
||||
) {
|
||||
|
||||
private val supportedMimes =
|
||||
Utils.getSupportedMimes()
|
||||
.filter { isCommitContentSupported(this.currentInputEditorInfo, it) }
|
||||
|
||||
/**
|
||||
* TODO
|
||||
*
|
||||
* @param file
|
||||
*/
|
||||
fun sendSticker(file: File) {
|
||||
val stickerType = Utils.getMimeType(file)
|
||||
if (stickerType == null || stickerType !in this.supportedMimes) {
|
||||
CoroutineScope(Dispatchers.Main).launch { doFallbackCommitContent(file) }
|
||||
return
|
||||
}
|
||||
doCommitContent(stickerType, file)
|
||||
}
|
||||
|
||||
/**
|
||||
* In the event that a mimetype is unsupported by a InputConnectionCompat (looking at you,
|
||||
* Signal) create a temporary png and send that. In the event that png is not supported, alert
|
||||
* the user.
|
||||
*
|
||||
* @param file: File
|
||||
*/
|
||||
private suspend fun doFallbackCommitContent(file: File) {
|
||||
// PNG might not be supported
|
||||
if ("image/png" !in this.supportedMimes) {
|
||||
this.toaster.toast(context.getString(R.string.fallback_040, file.extension))
|
||||
return
|
||||
}
|
||||
// Create a new compatSticker and convert the sticker to png
|
||||
val compatStickerName = file.hashCode().toString()
|
||||
val compatSticker = File(this.internalDir, "__compatSticker__/$compatStickerName.png")
|
||||
if (!compatSticker.exists()) {
|
||||
// If the sticker doesn't exist then create
|
||||
compatSticker.parentFile?.mkdirs()
|
||||
try {
|
||||
val request =
|
||||
ImageRequest.Builder(this.context)
|
||||
.data(file)
|
||||
.target { result ->
|
||||
val bitmap = result.toBitmap()
|
||||
bitmap.compress(
|
||||
Bitmap.CompressFormat.PNG, 90, FileOutputStream(compatSticker)
|
||||
)
|
||||
}
|
||||
.build()
|
||||
this.imageLoader.execute(request)
|
||||
} catch (ignore: IOException) {
|
||||
this.toaster.toast(this.context.getString(R.string.fallback_041))
|
||||
}
|
||||
}
|
||||
// Send the compatSticker!
|
||||
doCommitContent("image/png", compatSticker)
|
||||
// Remove old stickers
|
||||
val remSticker = this.compatCache.add(compatStickerName)
|
||||
remSticker?.let { File(this.internalDir, "__compatSticker__/$remSticker.png").delete() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a sticker file to a InputConnectionCompat
|
||||
*
|
||||
* @param mimeType String
|
||||
* @param file File
|
||||
*/
|
||||
private fun doCommitContent(mimeType: String, file: File) {
|
||||
// ContentUri, ClipDescription, linkUri
|
||||
val inputContentInfoCompat =
|
||||
InputContentInfoCompat(
|
||||
FileProvider.getUriForFile(
|
||||
this.context,
|
||||
"com.fredhappyface.ewesticker.inputcontent",
|
||||
file
|
||||
),
|
||||
ClipDescription(file.name, arrayOf(mimeType)),
|
||||
null
|
||||
)
|
||||
// InputConnection, EditorInfo, InputContentInfoCompat, int flags, null opts
|
||||
if (this.currentInputConnection == null || this.currentInputEditorInfo == null) return
|
||||
InputConnectionCompat.commitContent(
|
||||
this.currentInputConnection,
|
||||
this.currentInputEditorInfo,
|
||||
inputContentInfoCompat,
|
||||
InputConnectionCompat.INPUT_CONTENT_GRANT_READ_URI_PERMISSION,
|
||||
null
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the sticker is supported by the receiver
|
||||
*
|
||||
* @param editorInfo: EditorInfo - the editor/ receiver
|
||||
* @param mimeType: String - the image mimetype
|
||||
* @return boolean - is the mimetype supported?
|
||||
*/
|
||||
private fun isCommitContentSupported(editorInfo: EditorInfo?, mimeType: String?): Boolean {
|
||||
editorInfo?.packageName ?: return false
|
||||
mimeType ?: return false
|
||||
this.currentInputConnection ?: return false
|
||||
EditorInfoCompat.getContentMimeTypes(editorInfo).forEach {
|
||||
if (ClipDescription.compareMimeTypes(mimeType, it)) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
}
|
@ -8,7 +8,7 @@ import android.widget.Toast
|
||||
* android.content.Context to the constructor and call the 'toast' function (others as below)
|
||||
* toaster.state keeps track of an error state or similar.
|
||||
*
|
||||
* @property context: android.content.Context. e.g. applicationContext
|
||||
* @property context: android.content.Context. e.g. baseContext
|
||||
*/
|
||||
class Toaster(private val context: Context) {
|
||||
private var state = 0
|
||||
@ -46,19 +46,6 @@ class Toaster(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Call toaster.toastOnNonZero with a messages to create a toast notification of the state is
|
||||
* not zero. Context is set when Toaster is instantiated. Duration is determined based on
|
||||
* text length.
|
||||
*
|
||||
* @param string: String. Message to output
|
||||
*/
|
||||
fun toastOnNonZero(string: String) {
|
||||
if (this.state != 0) {
|
||||
toast(string)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the state to some integer value
|
||||
*
|
||||
@ -72,4 +59,4 @@ class Toaster(private val context: Context) {
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
@ -52,6 +52,7 @@ Copyright © Randy Zhou</string>
|
||||
<string name="links_heading">Enlaces</string>
|
||||
<string name="links_text">"- Proyecto: https://github.com/FredHappyface/Android.EweSticker
|
||||
- Tutorial: https://github.com/FredHappyface/Android.EweSticker/blob/main/TUTORIAL.md
|
||||
- Ayuda: https://github.com/FredHappyface/Android.EweSticker/blob/main/HELP.md
|
||||
- Licencia: https://github.com/FredHappyface/Android.EweSticker/blob/main/LICENSE.md</string>
|
||||
<!-- Interactive Messages -->
|
||||
<string name="pref_000">Preferencias cambiadas.Actualizar el teclado para configurar para aplicar</string>
|
||||
|
@ -1,4 +1,4 @@
|
||||
<resources>
|
||||
<resources xmlns:tools="http://schemas.android.com/tools" tools:ignore="PluralsCandidate">
|
||||
<!-- App info -->
|
||||
<string name="app_name" translatable="false">EweSticker</string>
|
||||
<string name="pack_icon">Pack icon</string>
|
||||
@ -53,6 +53,7 @@ Copyright © Randy Zhou</string>
|
||||
<string name="links_heading">Links</string>
|
||||
<string name="links_text">"- Project: https://github.com/FredHappyface/Android.EweSticker
|
||||
- Tutorial: https://github.com/FredHappyface/Android.EweSticker/blob/main/TUTORIAL.md
|
||||
- Help: https://github.com/FredHappyface/Android.EweSticker/blob/main/HELP.md
|
||||
- License: https://github.com/FredHappyface/Android.EweSticker/blob/main/LICENSE.md</string>
|
||||
<!-- Interactive Messages -->
|
||||
<string name="pref_000">Preferences changed. Reload the keyboard for settings to apply</string>
|
||||
|
Loading…
x
Reference in New Issue
Block a user