react-native-icon-switcher 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/IconSwitcher.podspec +20 -0
- package/LICENSE +20 -0
- package/README.md +252 -2
- package/android/build.gradle +51 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcher.kt +380 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcherExceptions.kt +24 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcherModule.kt +67 -0
- package/android/src/main/java/com/iconswitcher/IconSwitcherPackage.kt +30 -0
- package/android/src/test/AndroidManifest.xml +39 -0
- package/android/src/test/java/com/iconswitcher/IconSwitcherTest.kt +392 -0
- package/app.plugin.js +1 -0
- package/ios/ISIconSwitcher.h +49 -0
- package/ios/ISIconSwitcher.m +205 -0
- package/ios/IconSwitcher.h +5 -0
- package/ios/IconSwitcher.mm +100 -0
- package/jest/index.d.ts +31 -0
- package/jest/index.js +98 -0
- package/lib/module/NativeIconSwitcher.js +5 -0
- package/lib/module/NativeIconSwitcher.js.map +1 -0
- package/lib/module/config.js +6 -0
- package/lib/module/config.js.map +1 -0
- package/lib/module/errors.js +28 -0
- package/lib/module/errors.js.map +1 -0
- package/lib/module/index.js +69 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/register.js +2 -0
- package/lib/module/register.js.map +1 -0
- package/lib/module/remote/index.js +119 -0
- package/lib/module/remote/index.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NativeIconSwitcher.d.ts +18 -0
- package/lib/typescript/src/NativeIconSwitcher.d.ts.map +1 -0
- package/lib/typescript/src/config.d.ts +4 -0
- package/lib/typescript/src/config.d.ts.map +1 -0
- package/lib/typescript/src/errors.d.ts +7 -0
- package/lib/typescript/src/errors.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +20 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/lib/typescript/src/register.d.ts +7 -0
- package/lib/typescript/src/register.d.ts.map +1 -0
- package/lib/typescript/src/remote/index.d.ts +17 -0
- package/lib/typescript/src/remote/index.d.ts.map +1 -0
- package/lib/typescript/src/types.d.ts +59 -0
- package/lib/typescript/src/types.d.ts.map +1 -0
- package/package.json +253 -4
- package/plugin/build/cli.d.ts +3 -0
- package/plugin/build/cli.js +89 -0
- package/plugin/build/core/assets.d.ts +80 -0
- package/plugin/build/core/assets.js +463 -0
- package/plugin/build/core/errors.d.ts +4 -0
- package/plugin/build/core/errors.js +11 -0
- package/plugin/build/core/validate.d.ts +6 -0
- package/plugin/build/core/validate.js +229 -0
- package/plugin/build/index.d.ts +6 -0
- package/plugin/build/index.js +134 -0
- package/plugin/build/load-config.d.ts +3 -0
- package/plugin/build/load-config.js +50 -0
- package/plugin/build/paths.d.ts +15 -0
- package/plugin/build/paths.js +153 -0
- package/plugin/build/sync.d.ts +12 -0
- package/plugin/build/sync.js +162 -0
- package/plugin/build/transforms/android.d.ts +5 -0
- package/plugin/build/transforms/android.js +212 -0
- package/plugin/build/transforms/errors.d.ts +4 -0
- package/plugin/build/transforms/errors.js +11 -0
- package/plugin/build/transforms/index.d.ts +4 -0
- package/plugin/build/transforms/index.js +12 -0
- package/plugin/build/transforms/ios.d.ts +4 -0
- package/plugin/build/transforms/ios.js +111 -0
- package/plugin/build/transforms/sampleConfigs.d.ts +3 -0
- package/plugin/build/transforms/sampleConfigs.js +33 -0
- package/plugin/build/transforms/types.d.ts +2 -0
- package/plugin/build/transforms/types.js +14 -0
- package/plugin/build/types.d.ts +61 -0
- package/plugin/build/types.js +5 -0
- package/src/NativeIconSwitcher.ts +17 -0
- package/src/config.ts +15 -0
- package/src/errors.ts +38 -0
- package/src/index.tsx +78 -0
- package/src/register.ts +4 -0
- package/src/remote/index.ts +142 -0
- package/src/types.ts +59 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
package com.iconswitcher
|
|
2
|
+
|
|
3
|
+
import android.content.ComponentName
|
|
4
|
+
import android.content.Context
|
|
5
|
+
import android.content.pm.ActivityInfo
|
|
6
|
+
import android.content.pm.ApplicationInfo
|
|
7
|
+
import android.content.pm.PackageInfo
|
|
8
|
+
import android.content.pm.PackageManager
|
|
9
|
+
import android.os.Bundle
|
|
10
|
+
import androidx.lifecycle.DefaultLifecycleObserver
|
|
11
|
+
import androidx.lifecycle.LifecycleOwner
|
|
12
|
+
import androidx.test.core.app.ApplicationProvider
|
|
13
|
+
import io.mockk.every
|
|
14
|
+
import io.mockk.mockk
|
|
15
|
+
import io.mockk.slot
|
|
16
|
+
import io.mockk.verify
|
|
17
|
+
import java.util.concurrent.CountDownLatch
|
|
18
|
+
import java.util.concurrent.TimeUnit
|
|
19
|
+
import org.junit.Assert.assertEquals
|
|
20
|
+
import org.junit.Assert.assertFalse
|
|
21
|
+
import org.junit.Assert.assertThrows
|
|
22
|
+
import org.junit.Assert.assertTrue
|
|
23
|
+
import org.junit.Before
|
|
24
|
+
import org.junit.Test
|
|
25
|
+
import org.junit.runner.RunWith
|
|
26
|
+
import org.robolectric.RobolectricTestRunner
|
|
27
|
+
import org.robolectric.annotation.Config
|
|
28
|
+
import org.robolectric.shadows.ShadowLooper
|
|
29
|
+
|
|
30
|
+
@RunWith(RobolectricTestRunner::class)
|
|
31
|
+
@Config(sdk = [33])
|
|
32
|
+
class IconSwitcherTest {
|
|
33
|
+
private lateinit var context: Context
|
|
34
|
+
|
|
35
|
+
@Before
|
|
36
|
+
fun setUp() {
|
|
37
|
+
context = ApplicationProvider.getApplicationContext()
|
|
38
|
+
context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
39
|
+
.edit()
|
|
40
|
+
.clear()
|
|
41
|
+
.apply()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
@Test
|
|
45
|
+
fun discovery_ignores_unmarked_aliases() {
|
|
46
|
+
val switcher = IconSwitcher(context)
|
|
47
|
+
assertEquals(listOf("default", "halloween"), switcher.iconNames)
|
|
48
|
+
assertFalse(switcher.iconNames.contains("UnmarkedAlias"))
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@Test
|
|
52
|
+
fun icon_names_follow_metadata_order_when_package_info_order_differs() {
|
|
53
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
54
|
+
val packageName = context.packageName
|
|
55
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(
|
|
56
|
+
packageName,
|
|
57
|
+
iconOrder = listOf("halloween", "zebra", "default"),
|
|
58
|
+
)
|
|
59
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo(
|
|
60
|
+
icons = "default,zebra,halloween",
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
val switcher = IconSwitcher(context, pm)
|
|
64
|
+
assertEquals(listOf("default", "zebra", "halloween"), switcher.iconNames)
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
@Test
|
|
68
|
+
fun set_in_background_mode_returns_pending_and_does_not_touch_package_manager() {
|
|
69
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
70
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(context.packageName)
|
|
71
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
72
|
+
|
|
73
|
+
val switcher = IconSwitcher(context, pm)
|
|
74
|
+
val result = switcher.set("halloween")
|
|
75
|
+
|
|
76
|
+
assertEquals(IconSwitcher.STATUS_PENDING, result.status)
|
|
77
|
+
verify(exactly = 0) { pm.setComponentEnabledSetting(any(), any(), any()) }
|
|
78
|
+
verify(exactly = 0) { pm.setComponentEnabledSettings(any()) }
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
@Test
|
|
82
|
+
fun apply_toggles_marked_aliases_with_dont_kill_app() {
|
|
83
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
84
|
+
val packageName = context.packageName
|
|
85
|
+
val aliases = robolectricPackageInfo(packageName)
|
|
86
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns aliases
|
|
87
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo(
|
|
88
|
+
applyOn = IconSwitcher.APPLY_ON_IMMEDIATE,
|
|
89
|
+
)
|
|
90
|
+
val settingsSlot = slot<List<PackageManager.ComponentEnabledSetting>>()
|
|
91
|
+
every { pm.setComponentEnabledSettings(capture(settingsSlot)) } returns Unit
|
|
92
|
+
every { pm.getComponentEnabledSetting(any()) } answers {
|
|
93
|
+
val component = firstArg<ComponentName>()
|
|
94
|
+
when (component.className) {
|
|
95
|
+
".IconSwitcher_halloween" -> PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
96
|
+
else -> PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
val switcher = IconSwitcher(context, pm)
|
|
101
|
+
switcher.applyIcon("halloween", deferred = false)
|
|
102
|
+
|
|
103
|
+
val settings = settingsSlot.captured
|
|
104
|
+
assertEquals(2, settings.size)
|
|
105
|
+
assertTrue(
|
|
106
|
+
settings.any {
|
|
107
|
+
it.componentName!!.className == ".IconSwitcher_halloween" &&
|
|
108
|
+
it.enabledState == PackageManager.COMPONENT_ENABLED_STATE_ENABLED &&
|
|
109
|
+
it.enabledFlags == PackageManager.DONT_KILL_APP
|
|
110
|
+
},
|
|
111
|
+
)
|
|
112
|
+
assertTrue(
|
|
113
|
+
settings.any {
|
|
114
|
+
it.componentName!!.className == ".IconSwitcher_default" &&
|
|
115
|
+
it.enabledState == PackageManager.COMPONENT_ENABLED_STATE_DISABLED &&
|
|
116
|
+
it.enabledFlags == PackageManager.DONT_KILL_APP
|
|
117
|
+
},
|
|
118
|
+
)
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@Test
|
|
122
|
+
fun read_back_mismatch_reports_os_rejected() {
|
|
123
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
124
|
+
val packageName = context.packageName
|
|
125
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(packageName)
|
|
126
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo(
|
|
127
|
+
applyOn = IconSwitcher.APPLY_ON_IMMEDIATE,
|
|
128
|
+
)
|
|
129
|
+
every { pm.getComponentEnabledSetting(any()) } returns PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
130
|
+
|
|
131
|
+
val switcher = IconSwitcher(context, pm)
|
|
132
|
+
|
|
133
|
+
assertThrows(ErrOsRejectedException::class.java) {
|
|
134
|
+
switcher.applyIcon("halloween", deferred = false)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
@Test
|
|
139
|
+
fun cold_start_keeps_pending_without_touching_package_manager() {
|
|
140
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
141
|
+
val packageName = context.packageName
|
|
142
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(packageName)
|
|
143
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
144
|
+
|
|
145
|
+
context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
146
|
+
.edit()
|
|
147
|
+
.putString(IconSwitcher.KEY_PENDING, "halloween")
|
|
148
|
+
.apply()
|
|
149
|
+
|
|
150
|
+
val switcher = IconSwitcher(context, pm)
|
|
151
|
+
switcher.onModuleCreate()
|
|
152
|
+
|
|
153
|
+
verify(exactly = 0) { pm.setComponentEnabledSetting(any(), any(), any()) }
|
|
154
|
+
verify(exactly = 0) { pm.setComponentEnabledSettings(any()) }
|
|
155
|
+
val prefs = context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
156
|
+
assertEquals("halloween", prefs.getString(IconSwitcher.KEY_PENDING, null))
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@Test
|
|
160
|
+
fun cold_start_pm_mismatch_queues_stored_current_as_pending() {
|
|
161
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
162
|
+
val packageName = context.packageName
|
|
163
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(packageName)
|
|
164
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
165
|
+
every { pm.getComponentEnabledSetting(any()) } answers {
|
|
166
|
+
val component = firstArg<ComponentName>()
|
|
167
|
+
if (component.className == ".IconSwitcher_default") {
|
|
168
|
+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
169
|
+
} else {
|
|
170
|
+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
175
|
+
.edit()
|
|
176
|
+
.putString(IconSwitcher.KEY_CURRENT, "halloween")
|
|
177
|
+
.apply()
|
|
178
|
+
|
|
179
|
+
val switcher = IconSwitcher(context, pm)
|
|
180
|
+
switcher.onModuleCreate()
|
|
181
|
+
|
|
182
|
+
verify(exactly = 0) { pm.setComponentEnabledSetting(any(), any(), any()) }
|
|
183
|
+
verify(exactly = 0) { pm.setComponentEnabledSettings(any()) }
|
|
184
|
+
val prefs = context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
185
|
+
assertEquals("halloween", prefs.getString(IconSwitcher.KEY_PENDING, null))
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@Test
|
|
189
|
+
fun on_stop_applies_pending_without_blocking_main_thread() {
|
|
190
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
191
|
+
val packageName = context.packageName
|
|
192
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(packageName)
|
|
193
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
194
|
+
|
|
195
|
+
context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
196
|
+
.edit()
|
|
197
|
+
.putString(IconSwitcher.KEY_PENDING, "halloween")
|
|
198
|
+
.apply()
|
|
199
|
+
|
|
200
|
+
var lifecycleObserver: DefaultLifecycleObserver? = null
|
|
201
|
+
val prefs = context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
202
|
+
val switcher = IconSwitcher(
|
|
203
|
+
context = context,
|
|
204
|
+
packageManager = pm,
|
|
205
|
+
packageName = packageName,
|
|
206
|
+
prefs = prefs,
|
|
207
|
+
lifecycleRegistrar = { lifecycleObserver = it },
|
|
208
|
+
isInForeground = { false },
|
|
209
|
+
)
|
|
210
|
+
|
|
211
|
+
val applyLatch = CountDownLatch(1)
|
|
212
|
+
val settingsSlot = slot<List<PackageManager.ComponentEnabledSetting>>()
|
|
213
|
+
every { pm.setComponentEnabledSettings(capture(settingsSlot)) } answers {
|
|
214
|
+
applyLatch.countDown()
|
|
215
|
+
}
|
|
216
|
+
every { pm.getComponentEnabledSetting(any()) } answers {
|
|
217
|
+
val component = firstArg<ComponentName>()
|
|
218
|
+
if (component.className == ".IconSwitcher_halloween") {
|
|
219
|
+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
220
|
+
} else {
|
|
221
|
+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
switcher.onModuleCreate()
|
|
226
|
+
val owner = mockk<LifecycleOwner>(relaxed = true)
|
|
227
|
+
lifecycleObserver!!.onStop(owner)
|
|
228
|
+
|
|
229
|
+
assertTrue(applyLatch.await(2, TimeUnit.SECONDS))
|
|
230
|
+
assertTrue(
|
|
231
|
+
settingsSlot.captured.any {
|
|
232
|
+
it.componentName!!.className == ".IconSwitcher_halloween" &&
|
|
233
|
+
it.enabledFlags == PackageManager.DONT_KILL_APP
|
|
234
|
+
},
|
|
235
|
+
)
|
|
236
|
+
var pendingCleared = false
|
|
237
|
+
for (attempt in 0 until 50) {
|
|
238
|
+
if (!prefs.contains(IconSwitcher.KEY_PENDING)) {
|
|
239
|
+
pendingCleared = true
|
|
240
|
+
break
|
|
241
|
+
}
|
|
242
|
+
Thread.sleep(20)
|
|
243
|
+
}
|
|
244
|
+
assertTrue(pendingCleared)
|
|
245
|
+
assertEquals("halloween", prefs.getString(IconSwitcher.KEY_CURRENT, null))
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
@Test
|
|
249
|
+
fun apply_pending_none_when_nothing_pending() {
|
|
250
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
251
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(context.packageName)
|
|
252
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
253
|
+
|
|
254
|
+
val switcher = switcherWithForeground(pm, inForeground = false)
|
|
255
|
+
val result = switcher.applyPendingNow()
|
|
256
|
+
|
|
257
|
+
assertEquals(IconSwitcher.STATUS_NONE, result.status)
|
|
258
|
+
verify(exactly = 0) { pm.setComponentEnabledSetting(any(), any(), any()) }
|
|
259
|
+
verify(exactly = 0) { pm.setComponentEnabledSettings(any()) }
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
@Test
|
|
263
|
+
fun apply_pending_in_foreground_throws_and_keeps_pending() {
|
|
264
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
265
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(context.packageName)
|
|
266
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
267
|
+
|
|
268
|
+
val prefs = context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
269
|
+
prefs.edit().putString(IconSwitcher.KEY_PENDING, "halloween").apply()
|
|
270
|
+
|
|
271
|
+
val switcher = switcherWithForeground(pm, inForeground = true)
|
|
272
|
+
|
|
273
|
+
assertThrows(ErrForegroundException::class.java) {
|
|
274
|
+
switcher.applyPendingNow()
|
|
275
|
+
}
|
|
276
|
+
verify(exactly = 0) { pm.setComponentEnabledSetting(any(), any(), any()) }
|
|
277
|
+
verify(exactly = 0) { pm.setComponentEnabledSettings(any()) }
|
|
278
|
+
assertEquals("halloween", prefs.getString(IconSwitcher.KEY_PENDING, null))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
@Test
|
|
282
|
+
fun apply_pending_in_background_applies_and_clears_pending() {
|
|
283
|
+
val pm = mockk<PackageManager>(relaxed = true)
|
|
284
|
+
val packageName = context.packageName
|
|
285
|
+
every { pm.getPackageInfo(any<String>(), any<Int>()) } returns robolectricPackageInfo(packageName)
|
|
286
|
+
every { pm.getApplicationInfo(any<String>(), any<Int>()) } returns robolectricApplicationInfo()
|
|
287
|
+
val settingsSlot = slot<List<PackageManager.ComponentEnabledSetting>>()
|
|
288
|
+
every { pm.setComponentEnabledSettings(capture(settingsSlot)) } returns Unit
|
|
289
|
+
every { pm.getComponentEnabledSetting(any()) } answers {
|
|
290
|
+
val component = firstArg<ComponentName>()
|
|
291
|
+
if (component.className == ".IconSwitcher_halloween") {
|
|
292
|
+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
293
|
+
} else {
|
|
294
|
+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
val prefs = context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE)
|
|
299
|
+
prefs.edit().putString(IconSwitcher.KEY_PENDING, "halloween").apply()
|
|
300
|
+
|
|
301
|
+
val switcher = switcherWithForeground(pm, inForeground = false)
|
|
302
|
+
val result = switcher.applyPendingNow()
|
|
303
|
+
|
|
304
|
+
assertEquals(IconSwitcher.STATUS_APPLIED, result.status)
|
|
305
|
+
assertTrue(
|
|
306
|
+
settingsSlot.captured.any {
|
|
307
|
+
it.componentName!!.className == ".IconSwitcher_halloween" &&
|
|
308
|
+
it.enabledFlags == PackageManager.DONT_KILL_APP
|
|
309
|
+
},
|
|
310
|
+
)
|
|
311
|
+
assertEquals(null, prefs.getString(IconSwitcher.KEY_PENDING, null))
|
|
312
|
+
assertEquals("halloween", prefs.getString(IconSwitcher.KEY_CURRENT, null))
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
@Test
|
|
316
|
+
fun on_module_create_from_background_thread_registers_lifecycle_observer() {
|
|
317
|
+
val switcher = IconSwitcher(context)
|
|
318
|
+
val error = java.util.concurrent.atomic.AtomicReference<Throwable?>(null)
|
|
319
|
+
val done = CountDownLatch(1)
|
|
320
|
+
Thread {
|
|
321
|
+
try {
|
|
322
|
+
switcher.onModuleCreate()
|
|
323
|
+
} catch (t: Throwable) {
|
|
324
|
+
error.set(t)
|
|
325
|
+
} finally {
|
|
326
|
+
done.countDown()
|
|
327
|
+
}
|
|
328
|
+
}.start()
|
|
329
|
+
assertTrue(done.await(2, TimeUnit.SECONDS))
|
|
330
|
+
assertEquals(null, error.get())
|
|
331
|
+
ShadowLooper.idleMainLooper()
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
private fun switcherWithForeground(
|
|
335
|
+
pm: PackageManager,
|
|
336
|
+
inForeground: Boolean,
|
|
337
|
+
prefs: android.content.SharedPreferences =
|
|
338
|
+
context.getSharedPreferences(IconSwitcher.PREFS_NAME, Context.MODE_PRIVATE),
|
|
339
|
+
): IconSwitcher {
|
|
340
|
+
return IconSwitcher(
|
|
341
|
+
context = context,
|
|
342
|
+
packageManager = pm,
|
|
343
|
+
packageName = context.packageName,
|
|
344
|
+
prefs = prefs,
|
|
345
|
+
lifecycleRegistrar = { },
|
|
346
|
+
isInForeground = { inForeground },
|
|
347
|
+
)
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
private fun robolectricApplicationInfo(
|
|
351
|
+
applyOn: String = IconSwitcher.APPLY_ON_BACKGROUND,
|
|
352
|
+
icons: String = "default,halloween",
|
|
353
|
+
): ApplicationInfo {
|
|
354
|
+
return ApplicationInfo().apply {
|
|
355
|
+
metaData = Bundle().apply {
|
|
356
|
+
putString(IconSwitcher.META_APPLY_ON, applyOn)
|
|
357
|
+
putString(IconSwitcher.META_ICONS, icons)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
private fun robolectricPackageInfo(
|
|
363
|
+
packageName: String,
|
|
364
|
+
iconOrder: List<String> = listOf("default", "halloween"),
|
|
365
|
+
): PackageInfo {
|
|
366
|
+
fun alias(classSuffix: String, iconName: String?, enabled: Boolean): ActivityInfo {
|
|
367
|
+
return ActivityInfo().apply {
|
|
368
|
+
name = classSuffix
|
|
369
|
+
this.packageName = packageName
|
|
370
|
+
this.enabled = enabled
|
|
371
|
+
if (iconName != null) {
|
|
372
|
+
metaData = Bundle().apply { putString(IconSwitcher.META_ICON, iconName) }
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
val activities = iconOrder.map { iconName ->
|
|
378
|
+
val suffix = when (iconName) {
|
|
379
|
+
"default" -> ".IconSwitcher_default"
|
|
380
|
+
"halloween" -> ".IconSwitcher_halloween"
|
|
381
|
+
"zebra" -> ".IconSwitcher_zebra"
|
|
382
|
+
else -> ".IconSwitcher_$iconName"
|
|
383
|
+
}
|
|
384
|
+
alias(suffix, iconName, iconName == "default")
|
|
385
|
+
} + alias(".UnmarkedAlias", null, false)
|
|
386
|
+
|
|
387
|
+
return PackageInfo().apply {
|
|
388
|
+
this.packageName = packageName
|
|
389
|
+
this.activities = activities.toTypedArray()
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./plugin/build');
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
#import <Foundation/Foundation.h>
|
|
2
|
+
|
|
3
|
+
NS_ASSUME_NONNULL_BEGIN
|
|
4
|
+
|
|
5
|
+
FOUNDATION_EXPORT NSString *const ISIconSwitcherErrorDomain;
|
|
6
|
+
|
|
7
|
+
typedef NS_ERROR_ENUM(ISIconSwitcherErrorDomain, ISIconSwitcherErrorCode){
|
|
8
|
+
ISIconSwitcherErrorCodeUnsupported = 1,
|
|
9
|
+
ISIconSwitcherErrorCodeUnknownIcon = 2,
|
|
10
|
+
ISIconSwitcherErrorCodeOSRejected = 3,
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/// Injectable abstraction so `ISIconSwitcher` is testable without `UIApplication`.
|
|
14
|
+
@protocol ISAlternateIconProviding <NSObject>
|
|
15
|
+
|
|
16
|
+
@property (nonatomic, readonly) BOOL supportsAlternateIcons;
|
|
17
|
+
@property (nonatomic, readonly, nullable) NSString *alternateIconName;
|
|
18
|
+
- (void)setAlternateIconName:(nullable NSString *)iconName
|
|
19
|
+
completionHandler:(void (^_Nullable)(NSError *_Nullable error))completionHandler;
|
|
20
|
+
|
|
21
|
+
@end
|
|
22
|
+
|
|
23
|
+
typedef void (^ISMainScheduler)(void (^block)(void));
|
|
24
|
+
|
|
25
|
+
@interface ISIconSwitcher : NSObject
|
|
26
|
+
|
|
27
|
+
@property (class, nonatomic, readonly) NSString *alternateIconPrefix;
|
|
28
|
+
@property (class, nonatomic, readonly) NSString *iconsPlistKey;
|
|
29
|
+
|
|
30
|
+
- (instancetype)init NS_UNAVAILABLE;
|
|
31
|
+
+ (instancetype)new NS_UNAVAILABLE;
|
|
32
|
+
- (instancetype)initWithProvider:(id<ISAlternateIconProviding>)provider
|
|
33
|
+
bundle:(NSBundle *)bundle
|
|
34
|
+
scheduleOnMain:(ISMainScheduler)scheduleOnMain NS_DESIGNATED_INITIALIZER;
|
|
35
|
+
+ (instancetype)defaultSwitcher;
|
|
36
|
+
|
|
37
|
+
@property (nonatomic, readonly, copy) NSArray<NSString *> *iconNames;
|
|
38
|
+
@property (nonatomic, readonly) BOOL isSupported;
|
|
39
|
+
|
|
40
|
+
- (NSString *)currentIconName;
|
|
41
|
+
- (void)setIconName:(NSString *)name
|
|
42
|
+
completion:(void (^)(NSError *_Nullable error))completion;
|
|
43
|
+
|
|
44
|
+
+ (NSString *)iconNameFromAlternateIconName:(nullable NSString *)alternate;
|
|
45
|
+
+ (nullable NSString *)alternateIconNameFromIconName:(NSString *)name;
|
|
46
|
+
|
|
47
|
+
@end
|
|
48
|
+
|
|
49
|
+
NS_ASSUME_NONNULL_END
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
#import "ISIconSwitcher.h"
|
|
2
|
+
|
|
3
|
+
#import <UIKit/UIKit.h>
|
|
4
|
+
|
|
5
|
+
NSString *const ISIconSwitcherErrorDomain = @"com.iconswitcher.IconSwitcher";
|
|
6
|
+
|
|
7
|
+
@interface ISUIApplicationAlternateIconProvider : NSObject <ISAlternateIconProviding>
|
|
8
|
+
@end
|
|
9
|
+
|
|
10
|
+
@implementation ISUIApplicationAlternateIconProvider
|
|
11
|
+
|
|
12
|
+
+ (instancetype)shared
|
|
13
|
+
{
|
|
14
|
+
static ISUIApplicationAlternateIconProvider *instance;
|
|
15
|
+
static dispatch_once_t onceToken;
|
|
16
|
+
dispatch_once(&onceToken, ^{
|
|
17
|
+
instance = [ISUIApplicationAlternateIconProvider new];
|
|
18
|
+
});
|
|
19
|
+
return instance;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
- (BOOL)supportsAlternateIcons
|
|
23
|
+
{
|
|
24
|
+
return UIApplication.sharedApplication.supportsAlternateIcons;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
- (NSString *)alternateIconName
|
|
28
|
+
{
|
|
29
|
+
return UIApplication.sharedApplication.alternateIconName;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
- (void)setAlternateIconName:(NSString *)iconName
|
|
33
|
+
completionHandler:(void (^)(NSError *_Nullable))completionHandler
|
|
34
|
+
{
|
|
35
|
+
[UIApplication.sharedApplication setAlternateIconName:iconName
|
|
36
|
+
completionHandler:completionHandler];
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@end
|
|
40
|
+
|
|
41
|
+
static id _runOnMain(id (^work)(void))
|
|
42
|
+
{
|
|
43
|
+
if ([NSThread isMainThread]) {
|
|
44
|
+
return work();
|
|
45
|
+
}
|
|
46
|
+
__block id result;
|
|
47
|
+
dispatch_sync(dispatch_get_main_queue(), ^{
|
|
48
|
+
result = work();
|
|
49
|
+
});
|
|
50
|
+
return result;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
@implementation ISIconSwitcher {
|
|
54
|
+
id<ISAlternateIconProviding> _provider;
|
|
55
|
+
NSBundle *_bundle;
|
|
56
|
+
ISMainScheduler _scheduleOnMain;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
+ (NSString *)alternateIconPrefix
|
|
60
|
+
{
|
|
61
|
+
return @"AppIcon-";
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
+ (NSString *)iconsPlistKey
|
|
65
|
+
{
|
|
66
|
+
return @"IconSwitcherIcons";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
- (instancetype)initWithProvider:(id<ISAlternateIconProviding>)provider
|
|
70
|
+
bundle:(NSBundle *)bundle
|
|
71
|
+
scheduleOnMain:(ISMainScheduler)scheduleOnMain
|
|
72
|
+
{
|
|
73
|
+
if (self = [super init]) {
|
|
74
|
+
_provider = provider;
|
|
75
|
+
_bundle = bundle;
|
|
76
|
+
_scheduleOnMain = [scheduleOnMain copy];
|
|
77
|
+
}
|
|
78
|
+
return self;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
+ (instancetype)defaultSwitcher
|
|
82
|
+
{
|
|
83
|
+
return [[self alloc] initWithProvider:[ISUIApplicationAlternateIconProvider shared]
|
|
84
|
+
bundle:NSBundle.mainBundle
|
|
85
|
+
scheduleOnMain:^(void (^block)(void)) {
|
|
86
|
+
dispatch_async(dispatch_get_main_queue(), block);
|
|
87
|
+
}];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
- (NSArray<NSString *> *)iconNames
|
|
91
|
+
{
|
|
92
|
+
id raw = [_bundle objectForInfoDictionaryKey:self.class.iconsPlistKey];
|
|
93
|
+
if ([raw isKindOfClass:[NSArray class]]) {
|
|
94
|
+
NSArray *names = (NSArray *)raw;
|
|
95
|
+
if (names.count > 0) {
|
|
96
|
+
return names;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return @[ @"default" ];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
- (BOOL)isSupported
|
|
103
|
+
{
|
|
104
|
+
return [_runOnMain(^{
|
|
105
|
+
return @(_provider.supportsAlternateIcons);
|
|
106
|
+
}) boolValue];
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
- (NSString *)currentIconName
|
|
110
|
+
{
|
|
111
|
+
return [_runOnMain(^{
|
|
112
|
+
return [self.class iconNameFromAlternateIconName:_provider.alternateIconName];
|
|
113
|
+
}) copy];
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
- (void)setIconName:(NSString *)name completion:(void (^)(NSError *_Nullable))completion
|
|
117
|
+
{
|
|
118
|
+
if (![_runOnMain(^{
|
|
119
|
+
return @(_provider.supportsAlternateIcons);
|
|
120
|
+
}) boolValue]) {
|
|
121
|
+
completion([NSError errorWithDomain:ISIconSwitcherErrorDomain
|
|
122
|
+
code:ISIconSwitcherErrorCodeUnsupported
|
|
123
|
+
userInfo:@{
|
|
124
|
+
NSLocalizedDescriptionKey :
|
|
125
|
+
@"Alternate app icons are not supported on this device.",
|
|
126
|
+
}]);
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (![self.iconNames containsObject:name]) {
|
|
131
|
+
completion([NSError errorWithDomain:ISIconSwitcherErrorDomain
|
|
132
|
+
code:ISIconSwitcherErrorCodeUnknownIcon
|
|
133
|
+
userInfo:@{
|
|
134
|
+
NSLocalizedDescriptionKey :
|
|
135
|
+
@"The requested icon name is not bundled in this app.",
|
|
136
|
+
}]);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if ([[self currentIconName] isEqualToString:name]) {
|
|
141
|
+
completion(nil);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
NSString *targetAlternate = [self.class alternateIconNameFromIconName:name];
|
|
146
|
+
ISMainScheduler scheduleOnMain = _scheduleOnMain;
|
|
147
|
+
id<ISAlternateIconProviding> provider = _provider;
|
|
148
|
+
// The provider holds the completion block until UIKit calls it, so the read-back must not
|
|
149
|
+
// capture it strongly.
|
|
150
|
+
__weak id<ISAlternateIconProviding> weakProvider = provider;
|
|
151
|
+
|
|
152
|
+
scheduleOnMain(^{
|
|
153
|
+
[provider setAlternateIconName:targetAlternate
|
|
154
|
+
completionHandler:^(NSError *_Nullable error) {
|
|
155
|
+
scheduleOnMain(^{
|
|
156
|
+
if (error != nil) {
|
|
157
|
+
completion([NSError errorWithDomain:ISIconSwitcherErrorDomain
|
|
158
|
+
code:ISIconSwitcherErrorCodeOSRejected
|
|
159
|
+
userInfo:@{
|
|
160
|
+
NSLocalizedDescriptionKey : error.localizedDescription,
|
|
161
|
+
}]);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
NSString *actualAlternate = weakProvider.alternateIconName;
|
|
166
|
+
BOOL matches = (targetAlternate == nil && actualAlternate == nil) ||
|
|
167
|
+
[actualAlternate isEqualToString:targetAlternate];
|
|
168
|
+
if (!matches) {
|
|
169
|
+
completion([NSError
|
|
170
|
+
errorWithDomain:ISIconSwitcherErrorDomain
|
|
171
|
+
code:ISIconSwitcherErrorCodeOSRejected
|
|
172
|
+
userInfo:@{
|
|
173
|
+
NSLocalizedDescriptionKey :
|
|
174
|
+
@"Alternate icon name mismatch after apply",
|
|
175
|
+
}]);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
completion(nil);
|
|
180
|
+
});
|
|
181
|
+
}];
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
+ (NSString *)iconNameFromAlternateIconName:(NSString *)alternate
|
|
186
|
+
{
|
|
187
|
+
if (alternate == nil) {
|
|
188
|
+
return @"default";
|
|
189
|
+
}
|
|
190
|
+
NSString *prefix = self.alternateIconPrefix;
|
|
191
|
+
if ([alternate hasPrefix:prefix]) {
|
|
192
|
+
return [alternate substringFromIndex:prefix.length];
|
|
193
|
+
}
|
|
194
|
+
return alternate;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
+ (NSString *)alternateIconNameFromIconName:(NSString *)name
|
|
198
|
+
{
|
|
199
|
+
if ([name isEqualToString:@"default"]) {
|
|
200
|
+
return nil;
|
|
201
|
+
}
|
|
202
|
+
return [self.alternateIconPrefix stringByAppendingString:name];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
@end
|