expo-app-icon 1.0.0
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/README.md +62 -0
- package/android/build.gradle +43 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerModule.kt +81 -0
- package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerPackage.kt +13 -0
- package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerReactActivityLifecycleListener.kt +224 -0
- package/android/src/main/java/expo/modules/dynamicappicon/ExpoAppIconChangerView.kt +8 -0
- package/app.plugin.js +1 -0
- package/build/ExpoAppIconChangerModule.d.ts +3 -0
- package/build/ExpoAppIconChangerModule.d.ts.map +1 -0
- package/build/ExpoAppIconChangerModule.js +5 -0
- package/build/ExpoAppIconChangerModule.js.map +1 -0
- package/build/index.d.ts +21 -0
- package/build/index.d.ts.map +1 -0
- package/build/index.js +21 -0
- package/build/index.js.map +1 -0
- package/build/index.web.d.ts +12 -0
- package/build/index.web.d.ts.map +1 -0
- package/build/index.web.js +16 -0
- package/build/index.web.js.map +1 -0
- package/build/types.d.ts +4 -0
- package/build/types.d.ts.map +1 -0
- package/build/types.js +2 -0
- package/build/types.js.map +1 -0
- package/expo-module.config.json +9 -0
- package/ios/ExpoAppIconChanger.podspec +27 -0
- package/ios/ExpoAppIconChangerModule.swift +34 -0
- package/package.json +59 -0
- package/plugin/build/android-resources.d.ts +89 -0
- package/plugin/build/android-resources.js +151 -0
- package/plugin/build/android.d.ts +18 -0
- package/plugin/build/android.js +203 -0
- package/plugin/build/apple.d.ts +14 -0
- package/plugin/build/apple.js +143 -0
- package/plugin/build/icons.d.ts +23 -0
- package/plugin/build/icons.js +62 -0
- package/plugin/build/index.d.ts +8 -0
- package/plugin/build/index.js +46 -0
- package/plugin/build/types.d.ts +62 -0
- package/plugin/build/types.js +2 -0
package/README.md
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# expo-app-icon
|
|
2
|
+
|
|
3
|
+
Programmatically change your app's icon at runtime in Expo, with proper Android adaptive-icon support.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```sh
|
|
8
|
+
npx expo install expo-app-icon
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Configure
|
|
12
|
+
|
|
13
|
+
Add the plugin to your app config and declare your icons. Each icon needs an `ios` and/or `android` image path:
|
|
14
|
+
|
|
15
|
+
```json
|
|
16
|
+
{
|
|
17
|
+
"expo": {
|
|
18
|
+
"plugins": [
|
|
19
|
+
[
|
|
20
|
+
"expo-app-icon",
|
|
21
|
+
{
|
|
22
|
+
"red": {
|
|
23
|
+
"ios": "./assets/icons/red.png",
|
|
24
|
+
"android": "./assets/icons/red.png"
|
|
25
|
+
},
|
|
26
|
+
"blue": {
|
|
27
|
+
"ios": "./assets/icons/blue.png",
|
|
28
|
+
"android": "./assets/icons/blue.png"
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
]
|
|
32
|
+
]
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Then create a new build (the plugin runs during prebuild):
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
npx expo prebuild --clean
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Usage
|
|
44
|
+
|
|
45
|
+
```ts
|
|
46
|
+
import { getAppIcon, setAppIcon } from "expo-app-icon";
|
|
47
|
+
|
|
48
|
+
// Get the name of the current icon ("DEFAULT" when none is set)
|
|
49
|
+
const current = getAppIcon();
|
|
50
|
+
|
|
51
|
+
// Switch to one of your configured icons by key
|
|
52
|
+
setAppIcon("red");
|
|
53
|
+
|
|
54
|
+
// Reset back to the default icon
|
|
55
|
+
setAppIcon(null);
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
## Notes
|
|
59
|
+
|
|
60
|
+
- Icon keys are the keys you define in the plugin config.
|
|
61
|
+
- Android icons are generated as adaptive icons. Source images are scaled into the adaptive safe zone, so design your artwork edge-to-edge — it will be centered automatically.
|
|
62
|
+
- Changing the icon on Android briefly closes and reopens the app.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
apply plugin: 'com.android.library'
|
|
2
|
+
|
|
3
|
+
group = 'expo.modules.dynamicappicon'
|
|
4
|
+
version = '0.6.0'
|
|
5
|
+
|
|
6
|
+
def expoModulesCorePlugin = new File(project(":expo-modules-core").projectDir.absolutePath, "ExpoModulesCorePlugin.gradle")
|
|
7
|
+
apply from: expoModulesCorePlugin
|
|
8
|
+
applyKotlinExpoModulesCorePlugin()
|
|
9
|
+
useCoreDependencies()
|
|
10
|
+
useExpoPublishing()
|
|
11
|
+
|
|
12
|
+
// If you want to use the managed Android SDK versions from expo-modules-core, set this to true.
|
|
13
|
+
// The Android SDK versions will be bumped from time to time in SDK releases and may introduce breaking changes in your module code.
|
|
14
|
+
// Most of the time, you may like to manage the Android SDK versions yourself.
|
|
15
|
+
def useManagedAndroidSdkVersions = false
|
|
16
|
+
if (useManagedAndroidSdkVersions) {
|
|
17
|
+
useDefaultAndroidSdkVersions()
|
|
18
|
+
} else {
|
|
19
|
+
buildscript {
|
|
20
|
+
// Simple helper that allows the root project to override versions declared by this library.
|
|
21
|
+
ext.safeExtGet = { prop, fallback ->
|
|
22
|
+
rootProject.ext.has(prop) ? rootProject.ext.get(prop) : fallback
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
project.android {
|
|
26
|
+
compileSdkVersion safeExtGet("compileSdkVersion", 34)
|
|
27
|
+
defaultConfig {
|
|
28
|
+
minSdkVersion safeExtGet("minSdkVersion", 21)
|
|
29
|
+
targetSdkVersion safeExtGet("targetSdkVersion", 34)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
android {
|
|
35
|
+
namespace "expo.modules.dynamicappicon"
|
|
36
|
+
defaultConfig {
|
|
37
|
+
versionCode 1
|
|
38
|
+
versionName "0.6.0"
|
|
39
|
+
}
|
|
40
|
+
lintOptions {
|
|
41
|
+
abortOnError false
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
package expo.modules.dynamicappicon
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import expo.modules.kotlin.modules.Module
|
|
5
|
+
import expo.modules.kotlin.modules.ModuleDefinition
|
|
6
|
+
|
|
7
|
+
class ExpoAppIconChangerModule : Module() {
|
|
8
|
+
|
|
9
|
+
override fun definition() = ModuleDefinition {
|
|
10
|
+
Name("ExpoAppIconChanger")
|
|
11
|
+
|
|
12
|
+
Function("setAppIcon") { name: String? ->
|
|
13
|
+
try {
|
|
14
|
+
SharedObject.packageName = context.packageName
|
|
15
|
+
SharedObject.pm = pm
|
|
16
|
+
SharedObject.shouldChangeIcon = true
|
|
17
|
+
|
|
18
|
+
var result: String
|
|
19
|
+
|
|
20
|
+
// The default icon is served by a dedicated alias so the real
|
|
21
|
+
// MainActivity is never disabled (see the config plugin).
|
|
22
|
+
val defaultIcon = context.packageName + ".MainActivity" + "DEFAULT"
|
|
23
|
+
|
|
24
|
+
if (name == null) {
|
|
25
|
+
// Resetting to default icon if nothing passed
|
|
26
|
+
var currentIcon =
|
|
27
|
+
if (!SharedObject.icon.isEmpty()) SharedObject.icon
|
|
28
|
+
else defaultIcon
|
|
29
|
+
|
|
30
|
+
SharedObject.classesToKill.add(currentIcon)
|
|
31
|
+
SharedObject.icon = defaultIcon
|
|
32
|
+
result = "DEFAULT"
|
|
33
|
+
} else {
|
|
34
|
+
var newIcon = context.packageName + ".MainActivity" + name
|
|
35
|
+
var currentIcon =
|
|
36
|
+
if (!SharedObject.icon.isEmpty()) SharedObject.icon
|
|
37
|
+
else defaultIcon
|
|
38
|
+
|
|
39
|
+
if (currentIcon == newIcon) {
|
|
40
|
+
return@Function name
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
SharedObject.classesToKill.add(currentIcon)
|
|
44
|
+
SharedObject.icon = newIcon
|
|
45
|
+
result = name
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// background the app to trigger icon change
|
|
49
|
+
try {
|
|
50
|
+
currentActivity.moveTaskToBack(true)
|
|
51
|
+
} catch (e: Exception) {
|
|
52
|
+
// do nothing
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return@Function result
|
|
56
|
+
} catch (e: Exception) {
|
|
57
|
+
return@Function false
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
Function("getAppIcon") {
|
|
62
|
+
val componentClass: String = currentActivity.componentName.className
|
|
63
|
+
val currentIcon: String =
|
|
64
|
+
if (SharedObject.icon.isNotEmpty()) SharedObject.icon else componentClass
|
|
65
|
+
val parts = currentIcon.split("MainActivity")
|
|
66
|
+
val currentIconName = if (parts.size > 1) parts[1] else ""
|
|
67
|
+
|
|
68
|
+
return@Function if (currentIconName.isEmpty() || currentIconName == "DEFAULT") "DEFAULT"
|
|
69
|
+
else currentIconName
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
private val context: Context
|
|
74
|
+
get() = requireNotNull(appContext.reactContext) { "React Application Context is null" }
|
|
75
|
+
|
|
76
|
+
private val currentActivity
|
|
77
|
+
get() = requireNotNull(appContext.activityProvider?.currentActivity)
|
|
78
|
+
|
|
79
|
+
private val pm
|
|
80
|
+
get() = requireNotNull(currentActivity.packageManager)
|
|
81
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
package expo.modules.dynamicappicon
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import expo.modules.core.interfaces.Package
|
|
5
|
+
import expo.modules.core.interfaces.ReactActivityLifecycleListener
|
|
6
|
+
|
|
7
|
+
class ExpoAppIconChangerPackage : Package {
|
|
8
|
+
override fun createReactActivityLifecycleListeners(
|
|
9
|
+
activityContext: Context
|
|
10
|
+
): List<ReactActivityLifecycleListener> {
|
|
11
|
+
return listOf(ExpoAppIconChangerReactActivityLifecycleListener())
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,224 @@
|
|
|
1
|
+
package expo.modules.dynamicappicon
|
|
2
|
+
|
|
3
|
+
import android.app.Activity
|
|
4
|
+
import android.content.ComponentName
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.pm.PackageManager
|
|
7
|
+
import android.os.Handler
|
|
8
|
+
import android.os.Looper
|
|
9
|
+
import android.util.Log
|
|
10
|
+
import expo.modules.core.interfaces.ReactActivityLifecycleListener
|
|
11
|
+
|
|
12
|
+
object SharedObject {
|
|
13
|
+
var packageName: String = ""
|
|
14
|
+
var classesToKill = ArrayList<String>()
|
|
15
|
+
var icon: String = ""
|
|
16
|
+
var pm: PackageManager? = null
|
|
17
|
+
var shouldChangeIcon: Boolean = false
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
class ExpoAppIconChangerReactActivityLifecycleListener : ReactActivityLifecycleListener {
|
|
21
|
+
private var currentActivity: Activity? = null
|
|
22
|
+
private val handler = Handler(Looper.getMainLooper())
|
|
23
|
+
|
|
24
|
+
override fun onPause(activity: Activity) {
|
|
25
|
+
currentActivity = activity
|
|
26
|
+
// Apply icon change immediately when app goes to background
|
|
27
|
+
if (SharedObject.shouldChangeIcon) {
|
|
28
|
+
applyIconChange(activity)
|
|
29
|
+
// Force close the app after icon change to ensure clean restart
|
|
30
|
+
handler.postDelayed(
|
|
31
|
+
{ forceCloseApp(activity) },
|
|
32
|
+
500
|
|
33
|
+
) // Small delay to ensure icon change completes
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
override fun onResume(activity: Activity) {
|
|
38
|
+
currentActivity = activity
|
|
39
|
+
// Repair any non-icon activities that were incorrectly disabled by older versions
|
|
40
|
+
repairIncorrectlyDisabledActivities(activity)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
override fun onDestroy(activity: Activity) {
|
|
44
|
+
if (SharedObject.shouldChangeIcon) {
|
|
45
|
+
applyIconChange(activity)
|
|
46
|
+
}
|
|
47
|
+
if (currentActivity === activity) {
|
|
48
|
+
currentActivity = null
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
private fun forceCloseApp(activity: Activity) {
|
|
53
|
+
try {
|
|
54
|
+
// Force close the app process to ensure clean restart
|
|
55
|
+
activity.finishAffinity()
|
|
56
|
+
android.os.Process.killProcess(android.os.Process.myPid())
|
|
57
|
+
} catch (e: Exception) {
|
|
58
|
+
Log.e("IconChange", "Error force closing app", e)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun applyIconChange(activity: Activity) {
|
|
63
|
+
SharedObject.icon.takeIf { it.isNotEmpty() }?.let { icon ->
|
|
64
|
+
val pm = SharedObject.pm ?: return
|
|
65
|
+
val newComponent = ComponentName(SharedObject.packageName, icon)
|
|
66
|
+
|
|
67
|
+
if (!doesComponentExist(newComponent)) {
|
|
68
|
+
SharedObject.shouldChangeIcon = false
|
|
69
|
+
return
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
// Get all launcher activities and disable all icon aliases except the new one
|
|
74
|
+
val packageInfo =
|
|
75
|
+
pm.getPackageInfo(
|
|
76
|
+
SharedObject.packageName,
|
|
77
|
+
PackageManager.GET_ACTIVITIES or
|
|
78
|
+
PackageManager.GET_DISABLED_COMPONENTS
|
|
79
|
+
)
|
|
80
|
+
|
|
81
|
+
// Only disable activities that are icon aliases (named MainActivity*).
|
|
82
|
+
// The bare MainActivity is deliberately excluded: it must stay
|
|
83
|
+
// enabled so tools that launch it by explicit component (e.g. the
|
|
84
|
+
// Expo dev client) keep working. Its launcher role is owned by the
|
|
85
|
+
// DEFAULT alias instead (see the config plugin).
|
|
86
|
+
val mainActivityPrefix = "${SharedObject.packageName}.MainActivity"
|
|
87
|
+
val bareMainActivity = "${SharedObject.packageName}.MainActivity"
|
|
88
|
+
|
|
89
|
+
packageInfo.activities?.forEach { activityInfo ->
|
|
90
|
+
val componentName = ComponentName(SharedObject.packageName, activityInfo.name)
|
|
91
|
+
val state = pm.getComponentEnabledSetting(componentName)
|
|
92
|
+
|
|
93
|
+
// Only manage the icon aliases, leave MainActivity and other activities alone
|
|
94
|
+
val isIconAlias = activityInfo.name.startsWith(mainActivityPrefix) &&
|
|
95
|
+
activityInfo.name != bareMainActivity
|
|
96
|
+
|
|
97
|
+
if (isIconAlias &&
|
|
98
|
+
activityInfo.name != icon &&
|
|
99
|
+
state != PackageManager.COMPONENT_ENABLED_STATE_DISABLED
|
|
100
|
+
) {
|
|
101
|
+
pm.setComponentEnabledSetting(
|
|
102
|
+
componentName,
|
|
103
|
+
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
|
|
104
|
+
PackageManager.DONT_KILL_APP
|
|
105
|
+
)
|
|
106
|
+
Log.i("IconChange", "Disabled component: ${activityInfo.name}")
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Enable the new icon
|
|
111
|
+
pm.setComponentEnabledSetting(
|
|
112
|
+
newComponent,
|
|
113
|
+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
|
114
|
+
PackageManager.DONT_KILL_APP
|
|
115
|
+
)
|
|
116
|
+
Log.i("IconChange", "Enabled new icon: $icon")
|
|
117
|
+
} catch (e: Exception) {
|
|
118
|
+
Log.e("IconChange", "Error during icon change", e)
|
|
119
|
+
} finally {
|
|
120
|
+
SharedObject.shouldChangeIcon = false
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Ensure at least one component is enabled
|
|
124
|
+
ensureAtLeastOneComponentEnabled(activity)
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
private fun ensureAtLeastOneComponentEnabled(context: Context) {
|
|
129
|
+
val pm = SharedObject.pm ?: return
|
|
130
|
+
val packageInfo =
|
|
131
|
+
pm.getPackageInfo(
|
|
132
|
+
SharedObject.packageName,
|
|
133
|
+
PackageManager.GET_ACTIVITIES or PackageManager.GET_DISABLED_COMPONENTS
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
val hasEnabledComponent =
|
|
137
|
+
packageInfo.activities?.any { activityInfo ->
|
|
138
|
+
val componentName = ComponentName(SharedObject.packageName, activityInfo.name)
|
|
139
|
+
pm.getComponentEnabledSetting(componentName) ==
|
|
140
|
+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED
|
|
141
|
+
}
|
|
142
|
+
?: false
|
|
143
|
+
|
|
144
|
+
if (!hasEnabledComponent) {
|
|
145
|
+
// Fall back to the DEFAULT alias (the launcher), not the bare
|
|
146
|
+
// MainActivity, which intentionally has no launcher filter.
|
|
147
|
+
val defaultAlias = "${SharedObject.packageName}.MainActivityDEFAULT"
|
|
148
|
+
val defaultComponent = ComponentName(SharedObject.packageName, defaultAlias)
|
|
149
|
+
try {
|
|
150
|
+
pm.setComponentEnabledSetting(
|
|
151
|
+
defaultComponent,
|
|
152
|
+
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
|
|
153
|
+
PackageManager.DONT_KILL_APP
|
|
154
|
+
)
|
|
155
|
+
Log.i("IconChange", "No active component found. Re-enabling $defaultAlias")
|
|
156
|
+
} catch (e: Exception) {
|
|
157
|
+
Log.e("IconChange", "Error enabling fallback DEFAULT alias", e)
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Repair activities that were incorrectly disabled by older versions of this library.
|
|
164
|
+
* Re-enables any non-icon-alias activities that are currently disabled.
|
|
165
|
+
*/
|
|
166
|
+
private fun repairIncorrectlyDisabledActivities(activity: Activity) {
|
|
167
|
+
val pm = activity.packageManager
|
|
168
|
+
val packageName = activity.packageName
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
val packageInfo =
|
|
172
|
+
pm.getPackageInfo(
|
|
173
|
+
packageName,
|
|
174
|
+
PackageManager.GET_ACTIVITIES or PackageManager.GET_DISABLED_COMPONENTS
|
|
175
|
+
)
|
|
176
|
+
|
|
177
|
+
val mainActivityPrefix = "${packageName}.MainActivity"
|
|
178
|
+
val bareMainActivity = "${packageName}.MainActivity"
|
|
179
|
+
|
|
180
|
+
packageInfo.activities?.forEach { activityInfo ->
|
|
181
|
+
// The bare MainActivity is not an icon alias and must stay enabled,
|
|
182
|
+
// so a previously-disabled MainActivity gets repaired here too.
|
|
183
|
+
val isIconAlias = activityInfo.name.startsWith(mainActivityPrefix) &&
|
|
184
|
+
activityInfo.name != bareMainActivity
|
|
185
|
+
|
|
186
|
+
// If it's NOT an icon alias, it should never have been disabled by us
|
|
187
|
+
if (!isIconAlias) {
|
|
188
|
+
val componentName = ComponentName(packageName, activityInfo.name)
|
|
189
|
+
val state = pm.getComponentEnabledSetting(componentName)
|
|
190
|
+
|
|
191
|
+
// Re-enable if it was explicitly disabled
|
|
192
|
+
if (state == PackageManager.COMPONENT_ENABLED_STATE_DISABLED) {
|
|
193
|
+
pm.setComponentEnabledSetting(
|
|
194
|
+
componentName,
|
|
195
|
+
PackageManager.COMPONENT_ENABLED_STATE_DEFAULT,
|
|
196
|
+
PackageManager.DONT_KILL_APP
|
|
197
|
+
)
|
|
198
|
+
Log.i("IconChange", "Repaired incorrectly disabled activity: ${activityInfo.name}")
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (e: Exception) {
|
|
203
|
+
Log.e("IconChange", "Error repairing disabled activities", e)
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** Check if a component exists in the manifest (including disabled ones). */
|
|
208
|
+
private fun doesComponentExist(componentName: ComponentName): Boolean {
|
|
209
|
+
return try {
|
|
210
|
+
val packageInfo =
|
|
211
|
+
SharedObject.pm?.getPackageInfo(
|
|
212
|
+
SharedObject.packageName,
|
|
213
|
+
PackageManager.GET_ACTIVITIES or PackageManager.GET_DISABLED_COMPONENTS
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
val activityExists =
|
|
217
|
+
packageInfo?.activities?.any { it.name == componentName.className } == true
|
|
218
|
+
|
|
219
|
+
activityExists
|
|
220
|
+
} catch (e: Exception) {
|
|
221
|
+
false
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
package/app.plugin.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require("./plugin/build");
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAppIconChangerModule.d.ts","sourceRoot":"","sources":["../src/ExpoAppIconChangerModule.ts"],"names":[],"mappings":";AAIA,wBAAyD"}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { requireNativeModule } from 'expo';
|
|
2
|
+
// It loads the native module object from the JSI or falls back to
|
|
3
|
+
// the bridge module (from NativeModulesProxy) if the remote debugger is on.
|
|
4
|
+
export default requireNativeModule('ExpoAppIconChanger');
|
|
5
|
+
//# sourceMappingURL=ExpoAppIconChangerModule.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ExpoAppIconChangerModule.js","sourceRoot":"","sources":["../src/ExpoAppIconChangerModule.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,MAAM,CAAC;AAE3C,kEAAkE;AAClE,4EAA4E;AAC5E,eAAe,mBAAmB,CAAC,oBAAoB,CAAC,CAAC","sourcesContent":["import { requireNativeModule } from 'expo';\n\n// It loads the native module object from the JSI or falls back to\n// the bridge module (from NativeModulesProxy) if the remote debugger is on.\nexport default requireNativeModule('ExpoAppIconChanger');\n"]}
|
package/build/index.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { DynamicAppIconRegistry } from "./types";
|
|
2
|
+
/**
|
|
3
|
+
* Union of the icon keys declared in the plugin config (widened to string when none).
|
|
4
|
+
*/
|
|
5
|
+
export type IconName = DynamicAppIconRegistry["IconName"];
|
|
6
|
+
/**
|
|
7
|
+
* Value reported / accepted for the project's default launcher icon.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DEFAULT_ICON: "DEFAULT";
|
|
10
|
+
/**
|
|
11
|
+
* Switch the launcher icon at runtime.
|
|
12
|
+
*
|
|
13
|
+
* @param name A configured icon key, or `null` to reset to the default icon.
|
|
14
|
+
* @returns The applied icon name, `"DEFAULT"`, or `false` if unsupported.
|
|
15
|
+
*/
|
|
16
|
+
export declare function setAppIcon(name: IconName | null): IconName | typeof DEFAULT_ICON | false;
|
|
17
|
+
/**
|
|
18
|
+
* Get the currently active icon name, or `"DEFAULT"` when none is set.
|
|
19
|
+
*/
|
|
20
|
+
export declare function getAppIcon(): IconName | typeof DEFAULT_ICON;
|
|
21
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAGtD;;GAEG;AACH,MAAM,MAAM,QAAQ,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;AAE1D;;GAEG;AACH,eAAO,MAAM,YAAY,EAAG,SAAkB,CAAC;AAE/C;;;;;GAKG;AACH,wBAAgB,UAAU,CACxB,IAAI,EAAE,QAAQ,GAAG,IAAI,GACpB,QAAQ,GAAG,OAAO,YAAY,GAAG,KAAK,CAExC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,QAAQ,GAAG,OAAO,YAAY,CAE3D"}
|
package/build/index.js
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import NativeAppIcon from "./ExpoAppIconChangerModule";
|
|
2
|
+
/**
|
|
3
|
+
* Value reported / accepted for the project's default launcher icon.
|
|
4
|
+
*/
|
|
5
|
+
export const DEFAULT_ICON = "DEFAULT";
|
|
6
|
+
/**
|
|
7
|
+
* Switch the launcher icon at runtime.
|
|
8
|
+
*
|
|
9
|
+
* @param name A configured icon key, or `null` to reset to the default icon.
|
|
10
|
+
* @returns The applied icon name, `"DEFAULT"`, or `false` if unsupported.
|
|
11
|
+
*/
|
|
12
|
+
export function setAppIcon(name) {
|
|
13
|
+
return NativeAppIcon.setAppIcon(name);
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Get the currently active icon name, or `"DEFAULT"` when none is set.
|
|
17
|
+
*/
|
|
18
|
+
export function getAppIcon() {
|
|
19
|
+
return NativeAppIcon.getAppIcon();
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,aAAa,MAAM,4BAA4B,CAAC;AAOvD;;GAEG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,SAAkB,CAAC;AAE/C;;;;;GAKG;AACH,MAAM,UAAU,UAAU,CACxB,IAAqB;IAErB,OAAO,aAAa,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;AACxC,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,aAAa,CAAC,UAAU,EAAE,CAAC;AACpC,CAAC","sourcesContent":["import type { DynamicAppIconRegistry } from \"./types\";\nimport NativeAppIcon from \"./ExpoAppIconChangerModule\";\n\n/**\n * Union of the icon keys declared in the plugin config (widened to string when none).\n */\nexport type IconName = DynamicAppIconRegistry[\"IconName\"];\n\n/**\n * Value reported / accepted for the project's default launcher icon.\n */\nexport const DEFAULT_ICON = \"DEFAULT\" as const;\n\n/**\n * Switch the launcher icon at runtime.\n *\n * @param name A configured icon key, or `null` to reset to the default icon.\n * @returns The applied icon name, `\"DEFAULT\"`, or `false` if unsupported.\n */\nexport function setAppIcon(\n name: IconName | null\n): IconName | typeof DEFAULT_ICON | false {\n return NativeAppIcon.setAppIcon(name);\n}\n\n/**\n * Get the currently active icon name, or `\"DEFAULT\"` when none is set.\n */\nexport function getAppIcon(): IconName | typeof DEFAULT_ICON {\n return NativeAppIcon.getAppIcon();\n}\n"]}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { DynamicAppIconRegistry } from "./types";
|
|
2
|
+
export type IconName = DynamicAppIconRegistry["IconName"];
|
|
3
|
+
export declare const DEFAULT_ICON: "DEFAULT";
|
|
4
|
+
/**
|
|
5
|
+
* Web has no launcher icon; this is a no-op that reports failure.
|
|
6
|
+
*/
|
|
7
|
+
export declare function setAppIcon(_name: IconName | null): IconName | typeof DEFAULT_ICON | false;
|
|
8
|
+
/**
|
|
9
|
+
* Web has no launcher icon; always reports the default.
|
|
10
|
+
*/
|
|
11
|
+
export declare function getAppIcon(): IconName | typeof DEFAULT_ICON;
|
|
12
|
+
//# sourceMappingURL=index.web.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,sBAAsB,EAAE,MAAM,SAAS,CAAC;AAEtD,MAAM,MAAM,QAAQ,GAAG,sBAAsB,CAAC,UAAU,CAAC,CAAC;AAE1D,eAAO,MAAM,YAAY,EAAG,SAAkB,CAAC;AAE/C;;GAEG;AACH,wBAAgB,UAAU,CACxB,KAAK,EAAE,QAAQ,GAAG,IAAI,GACrB,QAAQ,GAAG,OAAO,YAAY,GAAG,KAAK,CAGxC;AAED;;GAEG;AACH,wBAAgB,UAAU,IAAI,QAAQ,GAAG,OAAO,YAAY,CAG3D"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export const DEFAULT_ICON = "DEFAULT";
|
|
2
|
+
/**
|
|
3
|
+
* Web has no launcher icon; this is a no-op that reports failure.
|
|
4
|
+
*/
|
|
5
|
+
export function setAppIcon(_name) {
|
|
6
|
+
console.error("setAppIcon is not supported on web");
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Web has no launcher icon; always reports the default.
|
|
11
|
+
*/
|
|
12
|
+
export function getAppIcon() {
|
|
13
|
+
console.error("getAppIcon is not supported on web");
|
|
14
|
+
return DEFAULT_ICON;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=index.web.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.web.js","sourceRoot":"","sources":["../src/index.web.ts"],"names":[],"mappings":"AAIA,MAAM,CAAC,MAAM,YAAY,GAAG,SAAkB,CAAC;AAE/C;;GAEG;AACH,MAAM,UAAU,UAAU,CACxB,KAAsB;IAEtB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpD,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;GAEG;AACH,MAAM,UAAU,UAAU;IACxB,OAAO,CAAC,KAAK,CAAC,oCAAoC,CAAC,CAAC;IACpD,OAAO,YAAY,CAAC;AACtB,CAAC","sourcesContent":["import type { DynamicAppIconRegistry } from \"./types\";\n\nexport type IconName = DynamicAppIconRegistry[\"IconName\"];\n\nexport const DEFAULT_ICON = \"DEFAULT\" as const;\n\n/**\n * Web has no launcher icon; this is a no-op that reports failure.\n */\nexport function setAppIcon(\n _name: IconName | null\n): IconName | typeof DEFAULT_ICON | false {\n console.error(\"setAppIcon is not supported on web\");\n return false;\n}\n\n/**\n * Web has no launcher icon; always reports the default.\n */\nexport function getAppIcon(): IconName | typeof DEFAULT_ICON {\n console.error(\"getAppIcon is not supported on web\");\n return DEFAULT_ICON;\n}\n"]}
|
package/build/types.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
package/build/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"","sourcesContent":["export interface DynamicAppIconRegistry {\n IconName: string;\n}\n"]}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
require 'json'
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, '..', 'package.json')))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = 'ExpoAppIconChanger'
|
|
7
|
+
s.version = package['version']
|
|
8
|
+
s.summary = package['description']
|
|
9
|
+
s.description = package['description']
|
|
10
|
+
s.license = package['license']
|
|
11
|
+
s.author = package['author']
|
|
12
|
+
s.homepage = package['homepage']
|
|
13
|
+
s.platform = :ios, '13.0'
|
|
14
|
+
s.swift_version = '5.4'
|
|
15
|
+
s.source = { git: 'https://github.com/outsung/expo-dynamic-app-icon' }
|
|
16
|
+
s.static_framework = true
|
|
17
|
+
|
|
18
|
+
s.dependency 'ExpoModulesCore'
|
|
19
|
+
|
|
20
|
+
# Swift/Objective-C compatibility
|
|
21
|
+
s.pod_target_xcconfig = {
|
|
22
|
+
'DEFINES_MODULE' => 'YES',
|
|
23
|
+
'SWIFT_COMPILATION_MODE' => 'wholemodule'
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
s.source_files = "**/*.{h,m,swift}"
|
|
27
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import ExpoModulesCore
|
|
2
|
+
|
|
3
|
+
public class ExpoAppIconChangerModule: Module {
|
|
4
|
+
public func definition() -> ModuleDefinition {
|
|
5
|
+
|
|
6
|
+
Name("ExpoAppIconChanger")
|
|
7
|
+
|
|
8
|
+
Function("setAppIcon") { (name: String?) -> String in
|
|
9
|
+
self.setAppIcon(name)
|
|
10
|
+
|
|
11
|
+
// Return "DEFAULT" if name is nil or empty
|
|
12
|
+
return name ?? "DEFAULT"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Function("getAppIcon") { () -> String in
|
|
16
|
+
// Return the current alternate icon name or "DEFAULT" if none is set
|
|
17
|
+
return UIApplication.shared.alternateIconName ?? "DEFAULT"
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
private func setAppIcon(_ iconName: String?) {
|
|
22
|
+
if UIApplication.shared.responds(to: #selector(getter: UIApplication.supportsAlternateIcons)) && UIApplication.shared.supportsAlternateIcons {
|
|
23
|
+
let iconNameToUse = iconName?.isEmpty == false ? iconName : nil // If the icon name is nil or empty, reset to default
|
|
24
|
+
|
|
25
|
+
// Set the alternate icon or reset to the default icon
|
|
26
|
+
UIApplication.shared.setAlternateIconName(iconNameToUse, completionHandler: { error in
|
|
27
|
+
if let error = error {
|
|
28
|
+
// Handle error if necessary
|
|
29
|
+
print("Failed to set app icon: \(error.localizedDescription)")
|
|
30
|
+
}
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|