Merge branch 'feature/API34' into develop

This commit is contained in:
J-Jamet
2024-06-24 13:26:36 +02:00
22 changed files with 203 additions and 104 deletions

View File

@@ -1,3 +1,6 @@
KeePassDX(4.1.0)
* Upgrade to API 34 (Android 14)
KeePassDX(4.0.8)
* Fix graphical bug that prevented databases from being opened on some versions of Android #1848 #1850

View File

@@ -5,15 +5,14 @@ apply plugin: 'kotlin-kapt'
android {
namespace 'com.kunzisoft.keepass'
compileSdkVersion 33
buildToolsVersion "33.0.2"
compileSdkVersion 34
defaultConfig {
applicationId "com.kunzisoft.keepass"
minSdkVersion 15
targetSdkVersion 33
versionCode = 131
versionName = "4.0.8"
targetSdkVersion 34
versionCode = 132
versionName = "4.1.0"
multiDexEnabled true
testApplicationId = "com.kunzisoft.keepass.tests"

View File

@@ -9,6 +9,10 @@
android:anyDensity="true" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_DATA_SYNC" />
<uses-permission
android:name="android.permission.FOREGROUND_SERVICE_SPECIAL_USE"/>
<uses-permission
android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission
@@ -197,18 +201,27 @@
<service
android:name="com.kunzisoft.keepass.services.DatabaseTaskNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AttachmentFileNotificationService"
android:foregroundServiceType="dataSync"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.ClipboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<service
android:name="com.kunzisoft.keepass.services.AdvancedUnlockNotificationService"
android:foregroundServiceType="specialUse"
android:enabled="true"
android:exported="false" />
<!-- Receiver for Autofill -->
@@ -235,10 +248,6 @@
<action android:name="android.view.InputMethod" />
</intent-filter>
</service>
<service
android:name="com.kunzisoft.keepass.services.KeyboardEntryNotificationService"
android:enabled="true"
android:exported="false" />
<receiver
android:name="com.kunzisoft.keepass.receivers.DexModeReceiver"
android:exported="true">

View File

@@ -38,7 +38,11 @@ import android.graphics.RectF
import android.graphics.drawable.BitmapDrawable
import android.os.Build
import android.util.TypedValue
import android.view.*
import android.view.GestureDetector
import android.view.MotionEvent
import android.view.ScaleGestureDetector
import android.view.View
import android.view.ViewGroup
import android.view.animation.AccelerateDecelerateInterpolator
import android.view.animation.DecelerateInterpolator
import android.view.animation.Interpolator
@@ -202,7 +206,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
override fun onDown(e: MotionEvent): Boolean = true
override fun onScroll(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
distanceX: Float,
distanceY: Float
@@ -220,7 +224,7 @@ class Loupe(imageView: ImageView, container: ViewGroup) : View.OnTouchListener,
}
override fun onFling(
e1: MotionEvent,
e1: MotionEvent?,
e2: MotionEvent,
velocityX: Float,
velocityY: Float

View File

@@ -27,6 +27,7 @@ import android.net.Uri
import android.os.IBinder
import android.util.Base64
import android.util.Log
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.database.element.binary.BinaryData.Companion.BASE64_FLAG
import com.kunzisoft.keepass.model.CipherEncryptDatabase
import com.kunzisoft.keepass.services.AdvancedUnlockNotificationService
@@ -69,9 +70,11 @@ class CipherDatabaseAction(context: Context) {
@Synchronized
private fun attachService(performedAction: () -> Unit) {
applicationContext.registerReceiver(mAdvancedUnlockBroadcastReceiver, IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
})
ContextCompat.registerReceiver(applicationContext, mAdvancedUnlockBroadcastReceiver,
IntentFilter().apply {
addAction(AdvancedUnlockNotificationService.REMOVE_ADVANCED_UNLOCK_KEY_ACTION)
}, ContextCompat.RECEIVER_EXPORTED
)
mServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName?, serviceBinder: IBinder?) {
@@ -97,7 +100,7 @@ class CipherDatabaseAction(context: Context) {
private fun detachService() {
try {
applicationContext.unregisterReceiver(mAdvancedUnlockBroadcastReceiver)
} catch (e: Exception) {}
} catch (_: Exception) {}
mServiceConnection?.let {
AdvancedUnlockNotificationService.unbindService(applicationContext, it)

View File

@@ -40,6 +40,7 @@ import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat.shouldShowRequestPermissionRationale
import androidx.core.content.ContextCompat
import androidx.core.content.ContextCompat.RECEIVER_NOT_EXPORTED
import androidx.fragment.app.FragmentActivity
import androidx.lifecycle.lifecycleScope
import com.kunzisoft.keepass.R
@@ -330,11 +331,11 @@ class DatabaseTaskProvider(
}
}
}
context.registerReceiver(databaseTaskBroadcastReceiver,
ContextCompat.registerReceiver(context, databaseTaskBroadcastReceiver,
IntentFilter().apply {
addAction(DATABASE_START_TASK_ACTION)
addAction(DATABASE_STOP_TASK_ACTION)
}
}, RECEIVER_NOT_EXPORTED
)
// Check if a service is currently running else do nothing

View File

@@ -86,11 +86,19 @@ class AdvancedUnlockNotificationService : NotificationService() {
val notificationTimeoutMilliSecs = PreferencesUtil.getAdvancedUnlockTimeout(this)
// Not necessarily a foreground service
if (mTimerJob == null && notificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(notificationBuilder, notificationTimeoutMilliSecs) {
defineTimerJob(
notificationBuilder,
NotificationServiceType.ADVANCED_UNLOCK,
notificationTimeoutMilliSecs
) {
sendBroadcast(Intent(REMOVE_ADVANCED_UNLOCK_KEY_ACTION))
}
} else {
startForeground(notificationId, notificationBuilder.build())
startForegroundCompat(
notificationId,
notificationBuilder,
NotificationServiceType.ADVANCED_UNLOCK
)
}
return mActionTaskBinder

View File

@@ -36,13 +36,13 @@ import com.kunzisoft.keepass.model.AttachmentState
import com.kunzisoft.keepass.model.EntryAttachmentState
import com.kunzisoft.keepass.model.StreamDirection
import com.kunzisoft.keepass.tasks.BinaryDatabaseManager
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import com.kunzisoft.keepass.utils.UriUtil.getDocumentFile
import com.kunzisoft.keepass.utils.getParcelableExtraCompat
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.util.*
import java.util.LinkedList
import java.util.concurrent.CopyOnWriteArrayList
@@ -282,15 +282,21 @@ class AttachmentFileNotificationService: LockNotificationService() {
AttachmentState.ERROR -> {
ServiceCompat.stopForeground(this, ServiceCompat.STOP_FOREGROUND_DETACH)
try {
notificationManager?.notify(
attachmentNotification.notificationId,
builder.build()
)
checkNotificationsPermission(this) {
notificationManager?.notify(
attachmentNotification.notificationId,
builder.build()
)
}
} catch (e: SecurityException) {
Log.e(TAG, "Unable to notify the attachment state", e)
}
} else -> {
startForeground(attachmentNotification.notificationId, builder.build())
startForegroundCompat(
attachmentNotification.notificationId,
builder,
NotificationServiceType.ATTACHMENT
)
}
}
}

View File

@@ -19,16 +19,12 @@
*/
package com.kunzisoft.keepass.services
import android.Manifest
import android.app.Activity
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.widget.Toast
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.model.EntryInfo
import com.kunzisoft.keepass.otp.OtpEntryFields.OTP_TOKEN_FIELD
@@ -196,23 +192,29 @@ class ClipboardEntryNotificationService : LockNotificationService() {
//Get settings
val notificationTimeoutMilliSecs = PreferencesUtil.getClipboardTimeout(this)
if (notificationTimeoutMilliSecs != NEVER) {
defineTimerJob(builder, notificationTimeoutMilliSecs, {
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
// New auto generated value
if (generatedValue != newGeneratedValue) {
generatedValue = newGeneratedValue
clipboardHelper?.copyToClipboard(
fieldToCopy.label,
generatedValue,
fieldToCopy.isSensitive
)
defineTimerJob(
builder,
NotificationServiceType.CLIPBOARD,
notificationTimeoutMilliSecs,
{
val newGeneratedValue = fieldToCopy.getGeneratedValue(mEntryInfo)
// New auto generated value
if (generatedValue != newGeneratedValue) {
generatedValue = newGeneratedValue
clipboardHelper?.copyToClipboard(
fieldToCopy.label,
generatedValue,
fieldToCopy.isSensitive
)
}
},
{
stopNotificationAndSendLockIfNeeded()
// Clean password only if no next field
if (nextFields.size <= 0)
clipboardHelper?.cleanClipboard()
}
}) {
stopNotificationAndSendLockIfNeeded()
// Clean password only if no next field
if (nextFields.size <= 0)
clipboardHelper?.cleanClipboard()
}
)
} else {
// No timer
checkNotificationsPermission {
@@ -226,12 +228,11 @@ class ClipboardEntryNotificationService : LockNotificationService() {
}
private fun checkNotificationsPermission(action: () -> Unit) {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) {
action.invoke()
} else {
showPermissionErrorIfNeeded(this)
}
checkNotificationsPermission(
this,
PreferencesUtil.isClipboardNotificationsEnable(this),
action
)
}
override fun onTaskRemoved(rootIntent: Intent?) {
@@ -255,26 +256,14 @@ class ClipboardEntryNotificationService : LockNotificationService() {
const val EXTRA_CLIPBOARD_FIELDS = "EXTRA_CLIPBOARD_FIELDS"
const val ACTION_CLEAN_CLIPBOARD = "ACTION_CLEAN_CLIPBOARD"
private fun showPermissionErrorIfNeeded(context: Context) {
if (PreferencesUtil.isClipboardNotificationsEnable(context)) {
Toast.makeText(context, R.string.warning_copy_permission, Toast.LENGTH_LONG).show()
}
}
fun checkAndLaunchNotification(
activity: Activity,
entry: EntryInfo
) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
if (ContextCompat.checkSelfPermission(
activity,
Manifest.permission.POST_NOTIFICATIONS
) == PackageManager.PERMISSION_GRANTED) {
launchNotificationIfAllowed(activity, entry)
} else {
showPermissionErrorIfNeeded(activity)
}
} else {
checkNotificationsPermission(
activity,
PreferencesUtil.isClipboardNotificationsEnable(activity)
) {
launchNotificationIfAllowed(activity, entry)
}
}

View File

@@ -591,7 +591,11 @@ open class DatabaseTaskNotificationService : LockNotificationService(), Progress
}
// Create the notification
startForeground(notificationId, notificationBuilder.build())
startForegroundCompat(
notificationId,
notificationBuilder,
NotificationServiceType.DATABASE_TASK
)
}
private fun removeIntentData(intent: Intent?) {

View File

@@ -111,13 +111,18 @@ class KeyboardEntryNotificationService : LockNotificationService() {
.setContentIntent(null)
.setDeleteIntent(pendingDeleteIntent)
notificationManager?.cancel(notificationId)
notificationManager?.notify(notificationId, builder.build())
checkNotificationsPermission(this, PreferencesUtil.isKeyboardNotificationEntryEnable(this)) {
notificationManager?.notify(notificationId, builder.build())
}
// Timeout only if notification clear is available
if (PreferencesUtil.isClearKeyboardNotificationEnable(this)) {
if (mNotificationTimeoutMilliSecs != TimeoutHelper.NEVER) {
defineTimerJob(builder, mNotificationTimeoutMilliSecs) {
defineTimerJob(
builder,
NotificationServiceType.KEYBOARD,
mNotificationTimeoutMilliSecs
) {
stopNotificationAndSendLockIfNeeded()
}
}

View File

@@ -1,17 +1,30 @@
package com.kunzisoft.keepass.services
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.Service
import android.content.Context
import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_DATA_SYNC
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_NONE
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_SPECIAL_USE
import android.os.Build
import android.os.IBinder
import android.util.TypedValue
import android.widget.Toast
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.activities.stylish.Stylish
import kotlinx.coroutines.*
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.cancel
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
abstract class NotificationService : Service() {
@@ -74,7 +87,32 @@ abstract class NotificationService : Service() {
}
}
protected fun startForegroundCompat(notificationId: Int,
builder: NotificationCompat.Builder,
type: NotificationServiceType
) {
@Suppress("DEPRECATION")
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val foregroundServiceTimer = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
FOREGROUND_SERVICE_TYPE_SPECIAL_USE
} else {
FOREGROUND_SERVICE_TYPE_NONE
}
val foregroundType = when (type) {
NotificationServiceType.DATABASE_TASK -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
NotificationServiceType.ATTACHMENT -> FOREGROUND_SERVICE_TYPE_DATA_SYNC
NotificationServiceType.CLIPBOARD -> foregroundServiceTimer
NotificationServiceType.KEYBOARD -> foregroundServiceTimer
NotificationServiceType.ADVANCED_UNLOCK -> foregroundServiceTimer
}
startForeground(notificationId, builder.build(), foregroundType)
} else {
startForeground(notificationId, builder.build())
}
}
protected fun defineTimerJob(builder: NotificationCompat.Builder,
type: NotificationServiceType,
timeoutMilliseconds: Long,
actionAfterASecond: (() -> Unit)? = null,
actionEnd: () -> Unit) {
@@ -87,7 +125,7 @@ abstract class NotificationService : Service() {
builder.setProgress(100,
(currentTime * 100 / timeoutInSeconds).toInt(),
false)
startForeground(notificationId, builder.build())
startForegroundCompat(notificationId, builder, type)
delay(1000)
if (currentTime <= 0) {
actionEnd()
@@ -114,5 +152,25 @@ abstract class NotificationService : Service() {
companion object {
private const val CHANNEL_ID = "com.kunzisoft.keepass.notification.channel"
private const val CHANNEL_NAME = "KeePassDX notification"
fun checkNotificationsPermission(
context: Context,
showError: Boolean = true,
action: () -> Unit
) {
if (ContextCompat.checkSelfPermission(context,
Manifest.permission.POST_NOTIFICATIONS)
== PackageManager.PERMISSION_GRANTED) {
action.invoke()
} else {
if (showError) {
Toast.makeText(
context,
R.string.warning_copy_permission,
Toast.LENGTH_LONG
).show()
}
}
}
}
}

View File

@@ -0,0 +1,9 @@
package com.kunzisoft.keepass.services
enum class NotificationServiceType {
DATABASE_TASK,
ATTACHMENT,
CLIPBOARD,
KEYBOARD,
ADVANCED_UNLOCK
}

View File

@@ -29,6 +29,7 @@ import android.content.Intent
import android.content.IntentFilter
import android.os.Build
import android.util.Log
import androidx.core.content.ContextCompat
import com.kunzisoft.keepass.R
import com.kunzisoft.keepass.database.ContextualDatabase
import com.kunzisoft.keepass.magikeyboard.MagikeyboardService
@@ -45,9 +46,9 @@ const val LOCK_ACTION = "com.kunzisoft.keepass.LOCK"
const val REMOVE_ENTRY_MAGIKEYBOARD_ACTION = "com.kunzisoft.keepass.REMOVE_ENTRY_MAGIKEYBOARD"
const val BACK_PREVIOUS_KEYBOARD_ACTION = "com.kunzisoft.keepass.BACK_PREVIOUS_KEYBOARD"
class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
class LockReceiver(private var lockAction: () -> Unit) : BroadcastReceiver() {
var mLockPendingIntent: PendingIntent? = null
private var mLockPendingIntent: PendingIntent? = null
var backToPreviousKeyboardAction: (() -> Unit)? = null
override fun onReceive(context: Context, intent: Intent) {
@@ -60,7 +61,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
}
Intent.ACTION_SCREEN_OFF -> {
if (PreferencesUtil.isLockDatabaseWhenScreenShutOffEnable(context)) {
mLockPendingIntent = PendingIntent.getBroadcast(context,
val lockPendingIntent = PendingIntent.getBroadcast(context,
4575,
Intent(intent).apply {
action = LOCK_ACTION
@@ -71,6 +72,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
0
}
)
this.mLockPendingIntent = lockPendingIntent
// Launch the effective action after a small time
val first: Long = System.currentTimeMillis() + context.getString(R.string.timeout_screen_off).toLong()
(context.getSystemService(ALARM_SERVICE) as AlarmManager?)?.let { alarmManager ->
@@ -80,20 +82,20 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
lockPendingIntent
)
} else {
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
lockPendingIntent
)
}
} else {
alarmManager.set(
AlarmManager.RTC_WAKEUP,
first,
mLockPendingIntent
lockPendingIntent
)
}
}
@@ -120,9 +122,9 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
}
private fun cancelLockPendingIntent(context: Context) {
mLockPendingIntent?.let {
mLockPendingIntent?.let { lockPendingIntent ->
val alarmManager = context.getSystemService(ALARM_SERVICE) as AlarmManager?
alarmManager?.cancel(mLockPendingIntent)
alarmManager?.cancel(lockPendingIntent)
mLockPendingIntent = null
}
}
@@ -131,7 +133,7 @@ class LockReceiver(var lockAction: () -> Unit) : BroadcastReceiver() {
fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
registerKeyboardAction: Boolean = false) {
lockReceiver?.let {
registerReceiver(it, IntentFilter().apply {
ContextCompat.registerReceiver(this, it, IntentFilter().apply {
addAction(Intent.ACTION_SCREEN_OFF)
addAction(Intent.ACTION_SCREEN_ON)
addAction(LOCK_ACTION)
@@ -139,7 +141,7 @@ fun Context.registerLockReceiver(lockReceiver: LockReceiver?,
addAction(REMOVE_ENTRY_MAGIKEYBOARD_ACTION)
addAction(BACK_PREVIOUS_KEYBOARD_ACTION)
}
})
}, ContextCompat.RECEIVER_EXPORTED)
}
}

View File

@@ -5,13 +5,12 @@ plugins {
android {
namespace 'com.kunzisoft.encrypt'
compileSdkVersion 33
buildToolsVersion "33.0.2"
compileSdkVersion 34
ndkVersion "21.4.7075529"
defaultConfig {
minSdkVersion 15
targetSdkVersion 33
targetSdkVersion 34
multiDexEnabled true
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"

View File

@@ -3,12 +3,11 @@ apply plugin: 'kotlin-android'
android {
namespace 'com.kunzisoft.keepass.database'
compileSdkVersion 33
buildToolsVersion "33.0.2"
compileSdkVersion 34
defaultConfig {
minSdkVersion 15
targetSdk 33
targetSdk 34
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@@ -0,0 +1 @@
* Upgrade to API 34 (Android 14)

View File

@@ -0,0 +1 @@
* Mise à jour vers API 34 (Android 14)

View File

@@ -3,12 +3,11 @@ apply plugin: 'kotlin-android'
android {
namespace 'com.kunzisoft.keepass.icon'
compileSdkVersion 33
buildToolsVersion '33.0.2'
compileSdkVersion 34
defaultConfig {
minSdkVersion 14
targetSdkVersion 33
targetSdkVersion 34
}
compileOptions {

View File

@@ -2,12 +2,11 @@ apply plugin: 'com.android.library'
android {
namespace 'com.kunzisoft.keepass.icon.classic'
compileSdkVersion 33
buildToolsVersion '33.0.2'
compileSdkVersion 34
defaultConfig {
minSdkVersion 14
targetSdkVersion 33
targetSdkVersion 34
}
resourcePrefix 'classic_'

View File

@@ -2,12 +2,11 @@ apply plugin: 'com.android.library'
android {
namespace 'com.kunzisoft.keepass.icon.material'
compileSdkVersion 33
buildToolsVersion '33.0.2'
compileSdkVersion 34
defaultConfig {
minSdkVersion 14
targetSdkVersion 33
targetSdkVersion 34
}
resourcePrefix 'material_'

View File

@@ -19,18 +19,19 @@
*/
package com.kunzisoft.keepass.icon
import android.annotation.SuppressLint
import android.content.Context
import android.content.res.Resources
import android.util.SparseIntArray
import java.util.*
import java.util.Locale
/**
* Class who construct dynamically database icons contains in a separate library
*
*
* It only supports icons with specific nomenclature **[stringId]_[%2d]_32dp**
* where [stringId] contains in a string xml attribute with id **resource_id** and
* [%2d] 2 numerical numbers between 00 and 68 included,
* It only supports icons with specific nomenclature **{stringId}_{%2d}_32dp**
* where {stringId} contains in a string xml attribute with id **resource_id** and
* {%2d} 2 numerical numbers between 00 and 68 included,
*
*
* See *icon-pack-classic* module as sample
@@ -41,6 +42,7 @@ import java.util.*
* @param packageName Context of the app to retrieve the resources
* @param resourceId String Id of the pack (ex : com.kunzisoft.keepass.icon.classic.R.string.resource_id)
*/
@SuppressLint("DiscouragedApi")
class IconPack(packageName: String, resources: Resources, resourceId: Int) {
private val icons: SparseIntArray = SparseIntArray()