Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 1 addition & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@

**iOS 🍏**
1. At the bottom of your `Info.plist` insert a key for `CFBundleIcons`
- Note: For iPad, you need to add the key `CFBundleIcons~ipad`
2. Within this dictionary add another key for `CFBundleAlternateIcons`
3. Finally then within this dictionary you can add in the keys for you new icons
- The `key` is the name you will reference from within code.
Expand Down Expand Up @@ -202,21 +201,4 @@ getIcon();
resetIcon();
```

> All functions are typed and return a promise that either resolves successfully, or will reject with the error that has occurred.

**react-native-push-notification and notifee**

When using `react-native-push-notification` or `notifee`, notifications won't work as we are using `activity-alias`.

To fix this, you need to create a Java file for each of the `activity-alias` in your `AndroidManifest.xml`.

The file should be placed alongside you `MainActivity.java`. Example:
```
android/app/src/main/java/com/myapp/MainActivity<KEY>.java
```
The content of this file should be:
```
package com.myapp;
public class MainActivity<KEY> extends MainActivity {}
```
Replace `<KEY>` with the icon name used in the manifest. Replace com.myapp with your android app structure.
> All functions are typed and return a promise that either resolves successfully, or will reject with the error that has occurred.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@

import android.app.Activity;
import android.app.Application;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.content.ComponentName;
import android.os.Bundle;
import android.util.Log;

import com.facebook.react.bridge.Promise;
import com.facebook.react.bridge.ReactApplicationContext;
Expand All @@ -19,11 +21,13 @@

@ReactModule(name = "ChangeIcon")
public class ChangeIconModule extends ReactContextBaseJavaModule implements Application.ActivityLifecycleCallbacks {
private static final String TAG = "ReactNativeChangeIcon";
public static final String NAME = "ChangeIcon";
private final String packageName;
private final Set<String> classesToKill = new HashSet<>();
private Boolean iconChanged = false;
private String componentClass = "";

private String _currentLaunchClassName = "";

public ChangeIconModule(ReactApplicationContext reactContext, String packageName) {
super(reactContext);
Expand All @@ -36,59 +40,79 @@ public String getName() {
return NAME;
}

public String getLaunchClassName() {
if (_currentLaunchClassName.isEmpty()) {
final Activity activity = getCurrentActivity();
final PackageManager packageManager = activity.getPackageManager();
try {
Intent intent = packageManager.getLaunchIntentForPackage(packageName);
_currentLaunchClassName = intent.resolveActivity(packageManager).getClassName();
if (_currentLaunchClassName.endsWith("Activity")) {
_currentLaunchClassName = _currentLaunchClassName + "Default";
}
} catch(Exception e) {
Log.d(TAG, e.getMessage());
}
}
return _currentLaunchClassName;
}

public String getNewLaunchClassName(String iconName) {
String currentLaunchClassName = getLaunchClassName();
if (currentLaunchClassName.isEmpty()) {
return "";
}

String[] activityNameSplit = currentLaunchClassName.split("Activity");

return activityNameSplit[0] + "Activity" + iconName;
}

@ReactMethod
public void getIcon(Promise promise) {
final Activity activity = getCurrentActivity();
if (activity == null) {
String launchClassName = getLaunchClassName();

if (launchClassName.isEmpty()) {
promise.reject("ANDROID:ACTIVITY_NOT_FOUND");
return;
}

final String activityName = activity.getComponentName().getClassName();

if (activityName.endsWith("MainActivity")) {
promise.resolve("Default");
return;
}
String[] activityNameSplit = activityName.split("MainActivity");
if (activityNameSplit.length != 2) {
promise.reject("ANDROID:UNEXPECTED_COMPONENT_CLASS:" + this.componentClass);
return;
}
String[] activityNameSplit = launchClassName.split("Activity");
promise.resolve(activityNameSplit[1]);
return;
}

@ReactMethod
public void changeIcon(String iconName, Promise promise) {
final Activity activity = getCurrentActivity();
final String activityName = activity.getComponentName().getClassName();
if (activity == null) {
Activity activity = getCurrentActivity();
String currentLaunchClassName = getLaunchClassName();

if (currentLaunchClassName.isEmpty()) {
promise.reject("ANDROID:ACTIVITY_NOT_FOUND");
return;
}
if (this.componentClass.isEmpty()) {
this.componentClass = activityName.endsWith("MainActivity") ? activityName + "Default" : activityName;
}

final String newIconName = (iconName == null || iconName.isEmpty()) ? "Default" : iconName;
final String activeClass = this.packageName + ".MainActivity" + newIconName;
if (this.componentClass.equals(activeClass)) {
promise.reject("ANDROID:ICON_ALREADY_USED:" + this.componentClass);
final String newLaunchClassName = getNewLaunchClassName(newIconName);
if (newLaunchClassName.isEmpty()) {
promise.reject("ANDROID:ACTIVITY_NOT_FOUND");
return;
}
if (currentLaunchClassName.equals(newLaunchClassName)) {
promise.reject("ANDROID:ICON_ALREADY_USED:" + currentLaunchClassName);
return;
}
try {
activity.getPackageManager().setComponentEnabledSetting(
new ComponentName(this.packageName, activeClass),
new ComponentName(this.packageName, newLaunchClassName),
PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
PackageManager.DONT_KILL_APP);
promise.resolve(newIconName);
} catch (Exception e) {
promise.reject("ANDROID:ICON_INVALID");
return;
}
this.classesToKill.add(this.componentClass);
this.componentClass = activeClass;
this.classesToKill.add(currentLaunchClassName);
this._currentLaunchClassName = newLaunchClassName;
activity.getApplication().registerActivityLifecycleCallbacks(this);
iconChanged = true;
}
Expand All @@ -99,8 +123,7 @@ private void completeIconChange() {
final Activity activity = getCurrentActivity();
if (activity == null)
return;

classesToKill.remove(componentClass);
classesToKill.remove(this._currentLaunchClassName);
classesToKill.forEach((cls) -> activity.getPackageManager().setComponentEnabledSetting(
new ComponentName(this.packageName, cls),
PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
Expand Down
81 changes: 47 additions & 34 deletions ios/ChangeIcon.mm
Original file line number Diff line number Diff line change
@@ -1,52 +1,65 @@
#import "ChangeIcon.h"
#import "ChangeIconCore.h"
#import <React/RCTUtils.h>

using namespace facebook::react;

@implementation ChangeIcon

RCT_EXPORT_MODULE()

+ (BOOL)requiresMainQueueSetup {
+ (BOOL)requiresMainQueueSetup
{
return NO;
}

RCT_REMAP_METHOD(getIcon, resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
NSString *currentIcon = [[UIApplication sharedApplication] alternateIconName];
if (currentIcon) {
resolve(currentIcon);
} else {
resolve(@"Default");
}
});
#ifdef RCT_NEW_ARCH_ENABLED
#else
RCT_EXPORT_METHOD(getIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
[ChangeIcon getIcon:resolve reject:reject];
}

RCT_REMAP_METHOD(changeIcon, iconName:(NSString *)iconName resolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject) {
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;
RCT_EXPORT_METHOD(changeIcon:(NSString *)iconName
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
[ChangeIcon changeIcon:iconName resolve:resolve reject:reject];
}

if ([[UIApplication sharedApplication] supportsAlternateIcons] == NO) {
reject(@"Error", @"IOS:NOT_SUPPORTED", error);
return;
}
RCT_EXPORT_METHOD(resetIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject)
{
[ChangeIcon resetIcon:resolve reject:reject];
}
#endif

NSString *currentIcon = [[UIApplication sharedApplication] alternateIconName];
// Code for the new architecture
#ifdef RCT_NEW_ARCH_ENABLED
- (std::shared_ptr<TurboModule>)getTurboModule:(const ObjCTurboModule::InitParams &)params
{
return std::make_shared<NativeChangeIconSpecJSI>(params);
}

if ([iconName isEqualToString:currentIcon]) {
reject(@"Error", @"IOS:ICON_ALREADY_USED", error);
return;
}
- (void)getIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[ChangeIconCore getIcon:resolve reject:reject];
}

NSString *newIconName;
if (iconName == nil || [iconName length] == 0 || [iconName isEqualToString:@"Default"]) {
newIconName = nil;
resolve(@"Default");
} else {
newIconName = iconName;
resolve(newIconName);
}
- (void)changeIcon:(NSString *)iconName
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[ChangeIconCore changeIcon:iconName resolve:resolve reject:reject];
}

[[UIApplication sharedApplication] setAlternateIconName:newIconName completionHandler:^(NSError * _Nullable error) {
return;
}];
});
- (void)resetIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[ChangeIconCore resetIcon:resolve reject:reject];
}
#endif

@end
6 changes: 6 additions & 0 deletions ios/ChangeIcon.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

/* Begin PBXBuildFile section */
5E555C0D2413F4C50049A1A2 /* ChangeIcon.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B5891CC2AC0600A0062D /* ChangeIcon.mm */; };
5E555C0E2413F4C50049A1A3 /* ChangeIconCore.mm in Sources */ = {isa = PBXBuildFile; fileRef = B3E7B58B1CC2AC0600A0062E /* ChangeIconCore.mm */; };
/* End PBXBuildFile section */

/* Begin PBXCopyFilesBuildPhase section */
Expand All @@ -26,6 +27,8 @@
134814201AA4EA6300B7C361 /* libChangeIcon.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libChangeIcon.a; sourceTree = BUILT_PRODUCTS_DIR; };
B3E7B5881CC2AC0600A0062D /* ChangeIcon.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChangeIcon.h; sourceTree = "<group>"; };
B3E7B5891CC2AC0600A0062D /* ChangeIcon.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChangeIcon.mm; sourceTree = "<group>"; };
B3E7B58A1CC2AC0600A0062E /* ChangeIconCore.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = ChangeIconCore.h; sourceTree = "<group>"; };
B3E7B58B1CC2AC0600A0062E /* ChangeIconCore.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = ChangeIconCore.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */

/* Begin PBXFrameworksBuildPhase section */
Expand All @@ -52,6 +55,8 @@
children = (
B3E7B5881CC2AC0600A0062D /* ChangeIcon.h */,
B3E7B5891CC2AC0600A0062D /* ChangeIcon.mm */,
B3E7B58A1CC2AC0600A0062E /* ChangeIconCore.h */,
B3E7B58B1CC2AC0600A0062E /* ChangeIconCore.mm */,
134814211AA4EA7D00B7C361 /* Products */,
);
sourceTree = "<group>";
Expand Down Expand Up @@ -114,6 +119,7 @@
buildActionMask = 2147483647;
files = (
B3E7B58A1CC2AC0600A0062D /* ChangeIcon.mm in Sources */,
5E555C0E2413F4C50049A1A3 /* ChangeIconCore.mm in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
Expand Down
17 changes: 17 additions & 0 deletions ios/ChangeIconCore.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
#import <Foundation/Foundation.h>
#import <React/RCTBridgeModule.h>
#import <UIKit/UIKit.h>

@interface ChangeIconCore : NSObject

+ (void)getIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

+ (void)changeIcon:(NSString *)iconName
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

+ (void)resetIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject;

@end
60 changes: 60 additions & 0 deletions ios/ChangeIconCore.mm
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#import "ChangeIconCore.h"

@implementation ChangeIconCore

+ (void)getIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
dispatch_async(dispatch_get_main_queue(), ^{
NSString *currentIcon = [[UIApplication sharedApplication] alternateIconName];
if (currentIcon) {
resolve(currentIcon);
} else {
resolve(@"Default");
}
});
}

+ (void)changeIcon:(NSString *)iconName
resolve:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
dispatch_async(dispatch_get_main_queue(), ^{
NSError *error = nil;

if ([[UIApplication sharedApplication] supportsAlternateIcons] == NO) {
reject(@"Error", @"IOS:NOT_SUPPORTED", error);
return;
}

NSString *currentIcon = [[UIApplication sharedApplication] alternateIconName];

if ([iconName isEqualToString:currentIcon]) {
reject(@"Error", @"IOS:ICON_ALREADY_USED", error);
return;
}

NSString *newIconName;
if (iconName == nil || [iconName length] == 0 || [iconName isEqualToString:@"Default"]) {
newIconName = nil;
resolve(@"Default");
} else {
newIconName = iconName;
resolve(newIconName);
}

[[UIApplication sharedApplication] setAlternateIconName:newIconName completionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"Error changing app icon: %@", error.localizedDescription);
}
}];
});
}

+ (void)resetIcon:(RCTPromiseResolveBlock)resolve
reject:(RCTPromiseRejectBlock)reject
{
[self changeIcon:@"Default" resolve:resolve reject:reject];
}

@end
Loading