├── Makefile ├── Prefs ├── APGRootListController.h ├── APGRootListController.m ├── Makefile ├── Resources │ ├── Info.plist │ ├── Root.plist │ ├── icon@2x.png │ └── icon@3x.png └── layout │ └── Library │ └── PreferenceLoader │ └── Preferences │ └── AirPodsGlyphPrefs.plist ├── Tweak ├── AirPodsGlyph.plist ├── Makefile ├── Tweak.h └── Tweak.x └── control /Makefile: -------------------------------------------------------------------------------- 1 | export TARGET = iphone:clang:latest:13.0 2 | export ARCHS = arm64 arm64e 3 | 4 | THEOS_DEVICE_IP = 10.12.14.60 5 | 6 | include $(THEOS)/makefiles/common.mk 7 | SUBPROJECTS += Tweak 8 | SUBPROJECTS += Prefs 9 | include $(THEOS_MAKE_PATH)/aggregate.mk 10 | -------------------------------------------------------------------------------- /Prefs/APGRootListController.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | 4 | @interface PSSpecifier () 5 | - (instancetype)initWithName:(NSString *)name 6 | target:(id)target 7 | set:(SEL)setter 8 | get:(SEL)getter 9 | detail:(Class)detailClass 10 | cell:(NSInteger)cellType 11 | edit:(Class)editClass; 12 | @end 13 | 14 | @interface MPAVRoute : NSObject 15 | @end 16 | 17 | @interface APGRootListController : PSListController 18 | - (void)disableAllExcept:(NSString *)except forPreferences:(NSMutableDictionary *)preferences; 19 | @end 20 | -------------------------------------------------------------------------------- /Prefs/APGRootListController.m: -------------------------------------------------------------------------------- 1 | #include "APGRootListController.h" 2 | 3 | @implementation APGRootListController 4 | 5 | - (NSArray *)specifiers { 6 | if (!_specifiers) { 7 | _specifiers = [self loadSpecifiersFromPlistName:@"Root" target:self]; 8 | } 9 | 10 | if ([MPAVRoute instancesRespondToSelector:@selector(isB298Route)]) { 11 | PSSpecifier *airpodsProSpecifier = 12 | [[PSSpecifier alloc] initWithName:@"Force AirPods Pro Glyph" 13 | target:self 14 | set:@selector(setPreferenceValue:specifier:) 15 | get:@selector(readPreferenceValue:) 16 | detail:nil 17 | cell:6 18 | edit:nil]; 19 | airpodsProSpecifier.properties[@"default"] = @NO; 20 | airpodsProSpecifier.properties[@"defaults"] = @"com.level3tjg.airpodsglyph"; 21 | airpodsProSpecifier.properties[@"key"] = @"forceAirpodsPro"; 22 | [_specifiers addObject:airpodsProSpecifier]; 23 | } 24 | 25 | if ([MPAVRoute instancesRespondToSelector:@selector(isB515Route)]) { 26 | PSSpecifier *airpodsMaxSpecifier = 27 | [[PSSpecifier alloc] initWithName:@"Force AirPods Max Glyph" 28 | target:self 29 | set:@selector(setPreferenceValue:specifier:) 30 | get:@selector(readPreferenceValue:) 31 | detail:nil 32 | cell:6 33 | edit:nil]; 34 | airpodsMaxSpecifier.properties[@"default"] = @NO; 35 | airpodsMaxSpecifier.properties[@"defaults"] = @"com.level3tjg.airpodsglyph"; 36 | airpodsMaxSpecifier.properties[@"key"] = @"forceAirpodsMax"; 37 | [_specifiers addObject:airpodsMaxSpecifier]; 38 | } 39 | 40 | if ([MPAVRoute instancesRespondToSelector:@selector(isB688Route)]) { 41 | PSSpecifier *airpodsGen3Specifier = 42 | [[PSSpecifier alloc] initWithName:@"Force AirPods Gen 3 Glyph" 43 | target:self 44 | set:@selector(setPreferenceValue:specifier:) 45 | get:@selector(readPreferenceValue:) 46 | detail:nil 47 | cell:6 48 | edit:nil]; 49 | airpodsGen3Specifier.properties[@"default"] = @NO; 50 | airpodsGen3Specifier.properties[@"defaults"] = @"com.level3tjg.airpodsglyph"; 51 | airpodsGen3Specifier.properties[@"key"] = @"forceAirpodsGen3"; 52 | [_specifiers addObject:airpodsGen3Specifier]; 53 | } 54 | 55 | return _specifiers; 56 | } 57 | 58 | - (void)disableAllExcept:(NSString *)except forPreferences:(NSMutableDictionary *)preferences { 59 | for (PSSpecifier *specifier in _specifiers) { 60 | NSString *key = specifier.properties[@"key"]; 61 | if (specifier.cellType == 6 && ![key isEqualToString:except]) { 62 | preferences[key] = @NO; 63 | UISwitch *control = specifier.properties[@"control"]; 64 | [control setOn:NO animated:YES]; 65 | } 66 | } 67 | } 68 | 69 | - (void)setPreferenceValue:(id)value specifier:(PSSpecifier *)specifier { 70 | NSString *key = specifier.properties[@"key"]; 71 | NSString *preferencesPath = [NSString 72 | stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]]; 73 | NSMutableDictionary *preferences = 74 | [NSMutableDictionary dictionaryWithContentsOfFile:preferencesPath]; 75 | [self disableAllExcept:key forPreferences:preferences]; 76 | preferences[key] = value; 77 | [preferences writeToFile:preferencesPath atomically:YES]; 78 | 79 | CFNotificationCenterPostNotification(CFNotificationCenterGetDarwinNotifyCenter(), 80 | CFSTR("com.level3tjg.airpodsglyph/prefsChanged"), NULL, NULL, 81 | false); 82 | } 83 | 84 | - (id)readPreferenceValue:(PSSpecifier *)specifier { 85 | NSString *preferencesPath = [NSString 86 | stringWithFormat:@"/User/Library/Preferences/%@.plist", specifier.properties[@"defaults"]]; 87 | NSDictionary *preferences = [NSDictionary dictionaryWithContentsOfFile:preferencesPath]; 88 | return preferences[specifier.properties[@"key"]]; 89 | } 90 | 91 | @end -------------------------------------------------------------------------------- /Prefs/Makefile: -------------------------------------------------------------------------------- 1 | include $(THEOS)/makefiles/common.mk 2 | 3 | BUNDLE_NAME = AirPodsGlyphPrefs 4 | 5 | $(BUNDLE_NAME)_FILES = APGRootListController.m 6 | $(BUNDLE_NAME)_FRAMEWORKS = UIKit MediaPlayer 7 | $(BUNDLE_NAME)_PRIVATE_FRAMEWORKS = Preferences 8 | $(BUNDLE_NAME)_INSTALL_PATH = /Library/PreferenceBundles 9 | $(BUNDLE_NAME)_CFLAGS = -fobjc-arc 10 | 11 | include $(THEOS_MAKE_PATH)/bundle.mk 12 | -------------------------------------------------------------------------------- /Prefs/Resources/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | English 7 | CFBundleExecutable 8 | AirPodsGlyphPrefs 9 | CFBundleIdentifier 10 | com.level3tjg.airpodsglyph 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundlePackageType 14 | BNDL 15 | CFBundleShortVersionString 16 | 1.0.0 17 | CFBundleSignature 18 | ???? 19 | CFBundleVersion 20 | 1.0 21 | NSPrincipalClass 22 | APGRootListController 23 | 24 | 25 | -------------------------------------------------------------------------------- /Prefs/Resources/Root.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | items 6 | 7 | 8 | cell 9 | PSGroupCell 10 | footerText 11 | Force glyphs for generic bluetooth audio devices. 12 | 13 | 14 | cell 15 | PSSwitchCell 16 | default 17 | 18 | defaults 19 | com.level3tjg.airpodsglyph 20 | key 21 | forceAirpods 22 | label 23 | Force AirPods Glyph 24 | 25 | 26 | title 27 | AirPodsGlyph 28 | 29 | 30 | -------------------------------------------------------------------------------- /Prefs/Resources/icon@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level3tjg/AirPodsGlyph/01c3e7754eaa6bd8a31ec980bb0205c6ae9eb8ef/Prefs/Resources/icon@2x.png -------------------------------------------------------------------------------- /Prefs/Resources/icon@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/level3tjg/AirPodsGlyph/01c3e7754eaa6bd8a31ec980bb0205c6ae9eb8ef/Prefs/Resources/icon@3x.png -------------------------------------------------------------------------------- /Prefs/layout/Library/PreferenceLoader/Preferences/AirPodsGlyphPrefs.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | entry 6 | 7 | bundle 8 | AirPodsGlyphPrefs 9 | cell 10 | PSLinkCell 11 | detail 12 | APGRootListController 13 | icon 14 | icon.png 15 | isController 16 | 17 | label 18 | AirPodsGlyph 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /Tweak/AirPodsGlyph.plist: -------------------------------------------------------------------------------- 1 | { Filter = { Bundles = ( "com.apple.springboard" ); }; } 2 | -------------------------------------------------------------------------------- /Tweak/Makefile: -------------------------------------------------------------------------------- 1 | INSTALL_TARGET_PROCESSES = SpringBoard 2 | 3 | include $(THEOS)/makefiles/common.mk 4 | 5 | TWEAK_NAME = AirPodsGlyph 6 | 7 | $(TWEAK_NAME)_FILES = Tweak.x 8 | $(TWEAK_NAME)_CFLAGS = -fobjc-arc 9 | $(TWEAK_NAME)_FRAMEWORKS = UIKit MediaPlayer 10 | $(TWEAK_NAME)_PRIVATE_FRAMEWORKS = MediaRemote 11 | 12 | include $(THEOS_MAKE_PATH)/tweak.mk -------------------------------------------------------------------------------- /Tweak/Tweak.h: -------------------------------------------------------------------------------- 1 | #import 2 | #import 3 | #import 4 | #import 5 | 6 | @interface AVOutputDevice : NSObject 7 | @end 8 | 9 | @interface AVOutputContext : NSObject 10 | - (AVOutputDevice *)outputDevice; 11 | @end 12 | 13 | @interface FBSystemService 14 | + (id)sharedInstance; 15 | - (void)exitAndRelaunch:(BOOL)arg1; 16 | @end 17 | 18 | @interface MRAVOutputDevice : NSObject 19 | @end 20 | 21 | @interface MRAVOutputDeviceSourceInfo : NSObject 22 | @end 23 | 24 | @interface MRAVConcreteOutputDevice : MRAVOutputDevice 25 | - (instancetype)initWithAVOutputDevice:(AVOutputDevice *)avOutputDevice 26 | sourceInfo:(MRAVOutputDeviceSourceInfo *)sourceInfo; 27 | @end 28 | 29 | @interface MPAVRoute : NSObject 30 | @property NSInteger routeSubtype; 31 | @property(getter=isHeadphonesRoute) BOOL headphonesRoute; 32 | @property(getter=isClusterRoute) BOOL clusterRoute; 33 | @property(getter=isAirpodsRoute) BOOL airpodsRoute; 34 | @property(getter=isB298Route) BOOL b298Route; 35 | @property(getter=isB515Route) BOOL b515Route; 36 | @property(getter=isB688Route) BOOL b688Route; 37 | @end 38 | 39 | @interface MPAVOutputDeviceRoute : MPAVRoute 40 | - (instancetype)initWithOutputDevices:(NSArray *)outputDevices; 41 | + (instancetype)systemAudioRoute; 42 | @end 43 | 44 | @interface MRAVOutputContext : NSObject 45 | @property AVOutputContext *avOutputContext; 46 | + (instancetype)sharedSystemAudioContext; 47 | @end 48 | 49 | @interface UIImage () 50 | + (instancetype)imageNamed:(NSString *)name inBundle:(NSBundle *)bundle; 51 | @end -------------------------------------------------------------------------------- /Tweak/Tweak.x: -------------------------------------------------------------------------------- 1 | #import "Tweak.h" 2 | 3 | static NSDictionary *prefs; 4 | 5 | %hook MPAVRoute 6 | %new 7 | - (BOOL)isHeadphonesRoute { 8 | return self.routeSubtype - 2 > 11; 9 | } 10 | - (BOOL)isAirpodsRoute { 11 | return (self.headphonesRoute && [prefs[@"forceAirpods"] boolValue]) || 12 | %orig; 13 | } 14 | - (BOOL)isB298Route { 15 | return (self.headphonesRoute && [prefs[@"forceAirpodsPro"] boolValue]) || 16 | %orig; 17 | } 18 | - (BOOL)isB515Route { 19 | return (self.headphonesRoute && [prefs[@"forceAirpodsMax"] boolValue]) || 20 | %orig; 21 | } 22 | - (BOOL)isB688Route { 23 | return (self.headphonesRoute && [prefs[@"forceAirpodsGen3"] boolValue]) || 24 | %orig; 25 | } 26 | %end 27 | 28 | %hook MPAVOutputDeviceRoute 29 | %new 30 | + (instancetype)systemAudioRoute { 31 | MRAVOutputContext *context = [MRAVOutputContext sharedSystemAudioContext]; 32 | if (!context || !context.avOutputContext) 33 | return nil; 34 | AVOutputDevice *avOutputDevice = [context.avOutputContext outputDevice]; 35 | if (!avOutputDevice) 36 | return nil; 37 | MRAVConcreteOutputDevice *outputDevice = [[MRAVConcreteOutputDevice alloc] 38 | initWithAVOutputDevice:avOutputDevice 39 | sourceInfo:[context valueForKey:@"_outputDeviceSourceInfo"]]; 40 | MPAVOutputDeviceRoute *route = 41 | [[MPAVOutputDeviceRoute alloc] initWithOutputDevices:@[ outputDevice ]]; 42 | return route; 43 | } 44 | %end 45 | 46 | %hook _UIStatusBarBluetoothItem 47 | - (UIImage *)imageForUpdate:(id)update { 48 | UIImage *image; 49 | MPAVRoute *route = [MPAVOutputDeviceRoute systemAudioRoute]; 50 | NSBundle *batteryCenterBundle = 51 | [NSBundle bundleWithIdentifier:@"com.apple.BatteryCenterUI"]; 52 | if (!batteryCenterBundle) 53 | batteryCenterBundle = 54 | [NSBundle bundleWithIdentifier:@"com.apple.BatteryCenter"]; 55 | if ([route respondsToSelector:@selector(isB688Route)] && route.b688Route) { 56 | image = [UIImage imageNamed:@"B688" inBundle:batteryCenterBundle]; 57 | if (!image) 58 | image = [UIImage systemImageNamed:@"airpods.gen3"]; 59 | } else if ([route respondsToSelector:@selector(isB515Route)] && 60 | route.b515Route) { 61 | image = [UIImage imageNamed:@"batteryglyphs-b515" 62 | inBundle:batteryCenterBundle]; 63 | if (!image) 64 | image = [UIImage systemImageNamed:@"airpodsmax"]; 65 | } else if ([route respondsToSelector:@selector(isB298Route)] && 66 | route.b298Route) { 67 | image = [UIImage imageNamed:@"batteryglyphs-b298-left-right" 68 | inBundle:batteryCenterBundle]; 69 | if (!image) 70 | image = [UIImage imageNamed:@"batteryglyphs-airpodspro-left-right" 71 | inBundle:batteryCenterBundle]; 72 | if (!image) 73 | image = [UIImage systemImageNamed:@"airpodspro"]; 74 | } else if ([route respondsToSelector:@selector(isAirpodsRoute)] && 75 | route.airpodsRoute) { 76 | image = [UIImage imageNamed:@"batteryglyphs-airpods-left-right" 77 | inBundle:batteryCenterBundle]; 78 | if (!image) 79 | image = [UIImage systemImageNamed:@"airpods"]; 80 | } 81 | if (!image) 82 | return %orig; 83 | return [image imageWithRenderingMode:UIImageRenderingModeAlwaysTemplate]; 84 | } 85 | %end 86 | 87 | %hook MediaControlsVolumeViewController 88 | - (void)viewDidLoad { 89 | %orig; 90 | [[NSNotificationCenter defaultCenter] 91 | addObserver:self 92 | selector:@selector(_updateGlyphPackageDescription) 93 | name:@"com.level3tjg.airpodsglyph/prefsChanged" 94 | object:nil]; 95 | } 96 | %end 97 | 98 | static void prefsChanged(CFNotificationCenterRef center, void *observer, 99 | CFStringRef name, const void *object, 100 | CFDictionaryRef userInfo) { 101 | prefs = [NSDictionary 102 | dictionaryWithContentsOfFile: 103 | @"/var/mobile/Library/Preferences/com.level3tjg.airpodsglyph.plist"]; 104 | [[NSNotificationCenter defaultCenter] 105 | postNotificationName:@"com.level3tjg.airpodsglyph/prefsChanged" 106 | object:nil]; 107 | } 108 | 109 | %ctor { 110 | CFNotificationCenterAddObserver( 111 | CFNotificationCenterGetDarwinNotifyCenter(), NULL, 112 | (CFNotificationCallback)prefsChanged, 113 | CFSTR("com.level3tjg.airpodsglyph/prefsChanged"), NULL, 114 | CFNotificationSuspensionBehaviorDeliverImmediately); 115 | prefsChanged(NULL, NULL, NULL, NULL, NULL); 116 | } 117 | -------------------------------------------------------------------------------- /control: -------------------------------------------------------------------------------- 1 | Package: com.level3tjg.airpodsglyph 2 | Name: AirPodsGlyph 3 | Version: 1.5 4 | Architecture: iphoneos-arm 5 | Description: Replace the Bluetooth headphones icon with an AirPods icon 6 | Depiction: https://level3tjg.me/repo/depictions/?p=com.level3tjg.airpodsglyph 7 | Maintainer: level3tjg 8 | Author: level3tjg 9 | Section: Tweaks 10 | Depends: mobilesubstrate, preferenceloader, firmware (>=13.0) 11 | --------------------------------------------------------------------------------