commit ec6ba52b586254dec3381a8dd541632f96f49676
parent 4e8dff949f1c67eb4bdd122198c869206b02e9a9
Author: rhunk <101876869+rhunk@users.noreply.github.com>
Date:   Thu, 27 Jul 2023 17:12:33 +0200

core subproject

Diffstat:
Mapp/build.gradle.kts | 28+++-------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/Logger.kt | 43-------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 122-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt | 24------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt | 126-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt | 111-------------------------------------------------------------------------------
Aapp/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt | 11+++++++++++
Dapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt | 42------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt | 85-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt | 280-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt | 114-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt | 243-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt | 215-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/map/MapActivity.kt | 98-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt | 87-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt | 112-------------------------------------------------------------------------------
Dapp/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt | 338-------------------------------------------------------------------------------
Mbuild.gradle.kts | 3+++
Acore/.gitignore | 16++++++++++++++++
Acore/build.gradle.kts | 45+++++++++++++++++++++++++++++++++++++++++++++
Rapp/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar -> core/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar | 0
Rapp/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar -> core/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar | 0
Rapp/libs/LSPosed-api-1.0-SNAPSHOT.jar -> core/libs/LSPosed-api-1.0-SNAPSHOT.jar | 0
Rapp/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl -> core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl | 0
Rapp/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl -> core/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl | 0
Rapp/src/main/assets/lang/ar_SA.json -> core/src/main/assets/lang/ar_SA.json | 0
Rapp/src/main/assets/lang/en_US.json -> core/src/main/assets/lang/en_US.json | 0
Rapp/src/main/assets/lang/fr_FR.json -> core/src/main/assets/lang/fr_FR.json | 0
Rapp/src/main/assets/lang/hi_IN.json -> core/src/main/assets/lang/hi_IN.json | 0
Rapp/src/main/assets/web/export_template.html -> core/src/main/assets/web/export_template.html | 0
Rapp/src/main/assets/web/rawinflate.js -> core/src/main/assets/web/rawinflate.js | 0
Rapp/src/main/assets/xposed_init -> core/src/main/assets/xposed_init | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/Constants.kt -> core/src/main/kotlin/me/rhunk/snapenhance/Constants.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/Logger.kt | 44++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt -> core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt | 123+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/XposedLoader.kt -> core/src/main/kotlin/me/rhunk/snapenhance/XposedLoader.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt -> core/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt -> core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt -> core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt -> core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt -> core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt | 24++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt -> core/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt | 126+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt -> core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt -> core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt -> core/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt -> core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/LocalePair.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/LocalePair.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/FriendActionButton.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/FriendActionButton.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/ScSize.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/ScSize.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapChapter.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapChapter.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapPlaylistItem.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapPlaylistItem.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt -> core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt -> core/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadMediaType.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadMediaType.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadStage.kt -> core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadStage.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt -> core/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt -> core/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt | 111+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AmoledDarkMode.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AmoledDarkMode.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/InfiniteStoryBoost.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/InfiniteStoryBoost.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt -> core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt -> core/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt -> core/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt -> core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt -> core/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt | 42++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt -> core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt -> core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt -> core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt | 280+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt | 114+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt | 243+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt | 215+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/download/MediaFilter.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/download/MediaFilter.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/map/MapActivity.kt | 98+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/AbstractMenu.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/AbstractMenu.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt | 87+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt -> core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt | 112+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt | 0
Acore/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt | 338+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt | 0
Rapp/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt -> core/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt | 0
Rapp/src/main/res/drawable/action_button_cancel.xml -> core/src/main/res/drawable/action_button_cancel.xml | 0
Rapp/src/main/res/drawable/action_button_success.xml -> core/src/main/res/drawable/action_button_success.xml | 0
Rapp/src/main/res/drawable/back_arrow.xml -> core/src/main/res/drawable/back_arrow.xml | 0
Rapp/src/main/res/drawable/bitmoji_blank.xml -> core/src/main/res/drawable/bitmoji_blank.xml | 0
Rapp/src/main/res/drawable/debug_settings_icon.xml -> core/src/main/res/drawable/debug_settings_icon.xml | 0
Rapp/src/main/res/drawable/download_manager_item_background.xml -> core/src/main/res/drawable/download_manager_item_background.xml | 0
Rapp/src/main/res/drawable/settings_icon.xml -> core/src/main/res/drawable/settings_icon.xml | 0
Rapp/src/main/res/font/avenir_next_bold.ttf -> core/src/main/res/font/avenir_next_bold.ttf | 0
Rapp/src/main/res/font/avenir_next_medium.ttf -> core/src/main/res/font/avenir_next_medium.ttf | 0
Rapp/src/main/res/layout/activity_default_header.xml -> core/src/main/res/layout/activity_default_header.xml | 0
Rapp/src/main/res/layout/config_activity.xml -> core/src/main/res/layout/config_activity.xml | 0
Rapp/src/main/res/layout/config_activity_debug_item.xml -> core/src/main/res/layout/config_activity_debug_item.xml | 0
Rapp/src/main/res/layout/config_activity_item.xml -> core/src/main/res/layout/config_activity_item.xml | 0
Rapp/src/main/res/layout/debug_setting_item.xml -> core/src/main/res/layout/debug_setting_item.xml | 0
Rapp/src/main/res/layout/debug_settings_page.xml -> core/src/main/res/layout/debug_settings_page.xml | 0
Rapp/src/main/res/layout/device_spoofer_activity.xml -> core/src/main/res/layout/device_spoofer_activity.xml | 0
Rapp/src/main/res/layout/download_manager_activity.xml -> core/src/main/res/layout/download_manager_activity.xml | 0
Rapp/src/main/res/layout/download_manager_item.xml -> core/src/main/res/layout/download_manager_item.xml | 0
Rapp/src/main/res/layout/map.xml -> core/src/main/res/layout/map.xml | 0
Rapp/src/main/res/layout/precise_location_dialog.xml -> core/src/main/res/layout/precise_location_dialog.xml | 0
Rapp/src/main/res/mipmap-anydpi-v26/launcher_icon.xml -> core/src/main/res/mipmap-anydpi-v26/launcher_icon.xml | 0
Rapp/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml -> core/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml | 0
Rapp/src/main/res/mipmap-hdpi/launcher_icon_foreground.png -> core/src/main/res/mipmap-hdpi/launcher_icon_foreground.png | 0
Rapp/src/main/res/mipmap-hdpi/launcher_icon_round.png -> core/src/main/res/mipmap-hdpi/launcher_icon_round.png | 0
Rapp/src/main/res/mipmap-mdpi/launcher_icon_foreground.png -> core/src/main/res/mipmap-mdpi/launcher_icon_foreground.png | 0
Rapp/src/main/res/mipmap-mdpi/launcher_icon_round.png -> core/src/main/res/mipmap-mdpi/launcher_icon_round.png | 0
Rapp/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png -> core/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png | 0
Rapp/src/main/res/mipmap-xhdpi/launcher_icon_round.png -> core/src/main/res/mipmap-xhdpi/launcher_icon_round.png | 0
Rapp/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png -> core/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png | 0
Rapp/src/main/res/mipmap-xxhdpi/launcher_icon_round.png -> core/src/main/res/mipmap-xxhdpi/launcher_icon_round.png | 0
Rapp/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png -> core/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png | 0
Rapp/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png -> core/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png | 0
Rapp/src/main/res/values/arrays.xml -> core/src/main/res/values/arrays.xml | 0
Rapp/src/main/res/values/colors.xml -> core/src/main/res/values/colors.xml | 0
Rapp/src/main/res/values/dimens.xml -> core/src/main/res/values/dimens.xml | 0
Rapp/src/main/res/values/launcher_icon_background.xml -> core/src/main/res/values/launcher_icon_background.xml | 0
Rapp/src/main/res/values/strings.xml -> core/src/main/res/values/strings.xml | 0
Rapp/src/main/res/values/themes.xml -> core/src/main/res/values/themes.xml | 0
Mgradle/libs.versions.toml | 2++
Msettings.gradle.kts | 2++
217 files changed, 2124 insertions(+), 2065 deletions(-)

diff --git a/app/build.gradle.kts b/app/build.gradle.kts @@ -6,9 +6,6 @@ plugins { alias(libs.plugins.kotlinAndroid) } -val appVersionName = "1.1.0" -val appVersionCode = 7 - android { namespace = "me.rhunk.snapenhance" compileSdk = 33 @@ -22,9 +19,6 @@ android { minSdk = 28 //noinspection OldTargetApi targetSdk = 33 - - versionCode = appVersionCode - versionName = appVersionName multiDexEnabled = true } @@ -66,7 +60,7 @@ android { applicationVariants.all { outputs.map { it as BaseVariantOutputImpl }.forEach { variant -> - variant.outputFileName = "app-${appVersionName}-${variant.name}.apk" + variant.outputFileName = "app-${rootProject.ext["appVersionName"]}-${variant.name}.apk" } } @@ -81,24 +75,8 @@ android { } dependencies { - compileOnly(files("libs/LSPosed-api-1.0-SNAPSHOT.jar")) - implementation(libs.coroutines) - implementation(libs.kotlin.reflect) - implementation(libs.recyclerview) - implementation(libs.gson) - implementation(libs.ffmpeg.kit) - implementation(libs.osmdroid.android) - implementation(libs.okhttp) - implementation(libs.androidx.documentfile) - - implementation(project(":mapper")) -} - -tasks.register("getVersion") { - doLast { - val versionFile = File("app/build/version.txt") - versionFile.writeText(android.defaultConfig.versionName.toString()) - } + implementation(project(":core")) + implementation(libs.androidx.activity.ktx) } afterEvaluate { diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/app/src/main/kotlin/me/rhunk/snapenhance/Logger.kt @@ -1,42 +0,0 @@ -package me.rhunk.snapenhance - -import android.util.Log -import de.robv.android.xposed.XposedBridge - -object Logger { - private const val TAG = "SnapEnhance" - - fun log(message: Any?) { - Log.i(TAG, message.toString()) - } - - fun debug(message: Any?) { - if (!BuildConfig.DEBUG) return - Log.d(TAG, message.toString()) - } - - fun error(throwable: Throwable) { - Log.e(TAG, "", throwable) - } - - fun error(message: Any?) { - Log.e(TAG, message.toString()) - } - - fun error(message: Any?, throwable: Throwable) { - Log.e(TAG, message.toString(), throwable) - } - - fun xposedLog(message: Any?) { - XposedBridge.log(message.toString()) - } - - fun xposedLog(message: Any?, throwable: Throwable?) { - XposedBridge.log(message.toString()) - XposedBridge.log(throwable) - } - - fun xposedLog(throwable: Throwable) { - XposedBridge.log(throwable) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/app/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -1,121 +0,0 @@ -package me.rhunk.snapenhance - -import android.app.Activity -import android.app.Application -import android.content.Context -import android.content.pm.PackageManager -import kotlinx.coroutines.runBlocking -import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.bridge.BridgeClient -import me.rhunk.snapenhance.data.SnapClassCache -import me.rhunk.snapenhance.hook.HookStage -import me.rhunk.snapenhance.hook.Hooker -import me.rhunk.snapenhance.hook.hook -import me.rhunk.snapenhance.util.getApplicationInfoCompat -import kotlin.time.ExperimentalTime -import kotlin.time.measureTime - -class SnapEnhance { - companion object { - lateinit var classLoader: ClassLoader - val classCache: SnapClassCache by lazy { - SnapClassCache(classLoader) - } - } - private val appContext = ModContext() - private var isBridgeInitialized = false - - init { - Hooker.hook(Application::class.java, "attach", HookStage.BEFORE) { param -> - appContext.androidContext = param.arg<Context>(0).also { - classLoader = it.classLoader - } - appContext.bridgeClient = BridgeClient(appContext) - - //for lspatch builds, we need to check if the service is correctly installed - runCatching { - appContext.androidContext.packageManager.getApplicationInfoCompat(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA) - }.onFailure { - appContext.crash("SnapEnhance bridge service is not installed. Please download stable version from https://github.com/rhunk/SnapEnhance/releases") - return@hook - } - - appContext.bridgeClient.apply { - start { bridgeResult -> - if (!bridgeResult) { - Logger.xposedLog("Cannot connect to bridge service") - appContext.softRestartApp() - return@start - } - runCatching { - runBlocking { - init() - } - }.onSuccess { - isBridgeInitialized = true - }.onFailure { - Logger.xposedLog("Failed to initialize", it) - } - } - } - } - - Activity::class.java.hook( "onCreate", HookStage.AFTER, { isBridgeInitialized }) { - val activity = it.thisObject() as Activity - if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook - val isMainActivityNotNull = appContext.mainActivity != null - appContext.mainActivity = activity - if (isMainActivityNotNull || !appContext.mappings.areMappingsLoaded) return@hook - onActivityCreate() - } - - var activityWasResumed = false - - //we need to reload the config when the app is resumed - Activity::class.java.hook("onResume", HookStage.AFTER, { isBridgeInitialized }) { - val activity = it.thisObject() as Activity - - if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook - - if (!activityWasResumed) { - activityWasResumed = true - return@hook - } - - Logger.debug("Reloading config") - appContext.config.loadFromBridge(appContext.bridgeClient) - } - } - - @OptIn(ExperimentalTime::class) - private suspend fun init() { - //load translations in a coroutine to speed up initialization - withContext(appContext.coroutineDispatcher) { - appContext.translation.loadFromBridge(appContext.bridgeClient) - } - - measureTime { - with(appContext) { - config.loadFromBridge(bridgeClient) - mappings.init() - //if mappings aren't loaded, we can't initialize features - if (!mappings.areMappingsLoaded) return - features.init() - } - }.also { time -> - Logger.debug("init took $time") - } - } - - @OptIn(ExperimentalTime::class) - private fun onActivityCreate() { - measureTime { - with(appContext) { - features.onActivityCreate() - actionManager.init() - } - }.also { time -> - Logger.debug("onActivityCreate took $time") - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt b/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt @@ -1,23 +0,0 @@ -package me.rhunk.snapenhance.action.impl - -import android.content.Intent -import android.os.Bundle -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.action.AbstractAction -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.ui.map.MapActivity - -class OpenMap: AbstractAction("action.open_map", dependsOnProperty = ConfigProperty.LOCATION_SPOOF) { - override fun run() { - context.runOnUiThread { - val mapActivityIntent = Intent() - mapActivityIntent.setClassName(BuildConfig.APPLICATION_ID, MapActivity::class.java.name) - mapActivityIntent.putExtra("location", Bundle().apply { - putDouble("latitude", context.config.string(ConfigProperty.LATITUDE).toDouble()) - putDouble("longitude", context.config.string(ConfigProperty.LONGITUDE).toDouble()) - }) - - context.mainActivity!!.startActivityForResult(mapActivityIntent, 0x1337) - } - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -1,126 +0,0 @@ -package me.rhunk.snapenhance.bridge - - -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.Build -import android.os.Handler -import android.os.HandlerThread -import android.os.IBinder -import de.robv.android.xposed.XposedHelpers -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.Logger.xposedLog -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.data.LocalePair -import java.util.concurrent.CompletableFuture -import java.util.concurrent.Executors -import kotlin.system.exitProcess - - -class BridgeClient( - private val context: ModContext -): ServiceConnection { - private lateinit var future: CompletableFuture<Boolean> - private lateinit var service: BridgeInterface - - fun start(callback: (Boolean) -> Unit) { - this.future = CompletableFuture() - - with(context.androidContext) { - //ensure the remote process is running - startActivity(Intent() - .setClassName(BuildConfig.APPLICATION_ID, ForceStartActivity::class.java.name) - .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) - ) - - val intent = Intent() - .setClassName(BuildConfig.APPLICATION_ID, BridgeService::class.java.name) - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { - bindService( - intent, - Context.BIND_AUTO_CREATE, - Executors.newSingleThreadExecutor(), - this@BridgeClient - ) - } else { - XposedHelpers.callMethod( - this, - "bindServiceAsUser", - intent, - this@BridgeClient, - Context.BIND_AUTO_CREATE, - Handler(HandlerThread("BridgeClient").apply { - start() - }.looper), - android.os.Process.myUserHandle() - ) - } - } - callback(future.get()) - } - - - override fun onServiceConnected(name: ComponentName, service: IBinder) { - this.service = BridgeInterface.Stub.asInterface(service) - future.complete(true) - } - - override fun onNullBinding(name: ComponentName) { - xposedLog("failed to connect to bridge service") - future.complete(false) - } - - override fun onServiceDisconnected(name: ComponentName) { - exitProcess(0) - } - - fun createAndReadFile( - fileType: BridgeFileType, - defaultContent: ByteArray - ): ByteArray = service.createAndReadFile(fileType.value, defaultContent) - - fun readFile(fileType: BridgeFileType): ByteArray = service.readFile(fileType.value) - - fun writeFile( - fileType: BridgeFileType, - content: ByteArray? - ): Boolean = service.writeFile(fileType.value, content) - - fun deleteFile(fileType: BridgeFileType) = service.deleteFile(fileType.value) - - - fun isFileExists(fileType: BridgeFileType) = service.isFileExists(fileType.value) - - fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit) - - fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? = service.getMessageLoggerMessage(conversationId, id) - - fun addMessageLoggerMessage(conversationId: String,id: Long, message: ByteArray) = service.addMessageLoggerMessage(conversationId, id, message) - - fun deleteMessageLoggerMessage(conversationId: String, id: Long) = service.deleteMessageLoggerMessage(conversationId, id) - - fun clearMessageLogger() = service.clearMessageLogger() - - fun fetchTranslations() = service.fetchTranslations().map { - LocalePair(it.key, it.value) - } - - fun getAutoUpdaterTime(): Long { - createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { - return if (isEmpty()) { - 0 - } else { - String(this).toLong() - } - } - } - - fun setAutoUpdaterTime(time: Long) { - writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) - } - - fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt b/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt @@ -1,110 +0,0 @@ -package me.rhunk.snapenhance.features.impl - -import android.annotation.SuppressLint -import android.app.AlertDialog -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.net.Uri -import android.os.Environment -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.features.Feature -import me.rhunk.snapenhance.features.FeatureLoadParams -import me.rhunk.snapenhance.ui.ViewAppearanceHelper -import okhttp3.OkHttpClient -import okhttp3.Request -import org.json.JSONArray - -class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { - override fun asyncOnActivityCreate() { - val checkForUpdateMode = context.config.state(ConfigProperty.AUTO_UPDATER) - val currentTimeMillis = System.currentTimeMillis() - val checkForUpdatesTimestamp = context.bridgeClient.getAutoUpdaterTime() - - val delayTimestamp = when (checkForUpdateMode) { - "EVERY_LAUNCH" -> currentTimeMillis - checkForUpdatesTimestamp - "DAILY" -> 86400000L - "WEEKLY" -> 604800000L - else -> return - } - - if (checkForUpdatesTimestamp + delayTimestamp > currentTimeMillis) return - - runCatching { - checkForUpdates() - }.onFailure { - Logger.error("Failed to check for updates: ${it.message}", it) - }.onSuccess { - context.bridgeClient.setAutoUpdaterTime(currentTimeMillis) - } - } - - @SuppressLint("UnspecifiedRegisterReceiverFlag") - fun checkForUpdates(): String? { - val endpoint = Request.Builder().url("https://api.github.com/repos/rhunk/SnapEnhance/releases").build() - val response = OkHttpClient().newCall(endpoint).execute() - - if (!response.isSuccessful) throw Throwable("Failed to fetch releases: ${response.code}") - - val releases = JSONArray(response.body.string()).also { - if (it.length() == 0) throw Throwable("No releases found") - } - - val latestRelease = releases.getJSONObject(0) - val latestVersion = latestRelease.getString("tag_name") - if (latestVersion.removePrefix("v") == BuildConfig.VERSION_NAME) return null - - val releaseContentBody = latestRelease.getString("body") - val downloadEndpoint = latestRelease.getJSONArray("assets").getJSONObject(0).getString("browser_download_url") - - context.runOnUiThread { - ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) - .setTitle(context.translation["auto_updater.dialog_title"]) - .setMessage( - context.translation.format("auto_updater.dialog_message", - "version" to latestVersion, - "body" to releaseContentBody) - ) - .setNegativeButton(context.translation["auto_updater.dialog_negative_button"]) { dialog, _ -> - dialog.dismiss() - } - .setPositiveButton(context.translation["auto_updater.dialog_positive_button"]) { dialog, _ -> - dialog.dismiss() - context.longToast(context.translation["auto_updater.downloading_toast"]) - - val request = DownloadManager.Request(Uri.parse(downloadEndpoint)) - .setTitle(context.translation["auto_updater.download_manager_notification_title"]) - .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "latest-snapenhance.apk") - .setMimeType("application/vnd.android.package-archive") - .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) - - val downloadManager = context.androidContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - val downloadId = downloadManager.enqueue(request) - - val onCompleteReceiver = object: BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) - if (id != downloadId) return - context.unregisterReceiver(this) - context.startActivity( - Intent(Intent.ACTION_VIEW).apply { - setDataAndType(downloadManager.getUriForDownloadedFile(downloadId), "application/vnd.android.package-archive") - flags = Intent.FLAG_ACTIVITY_NEW_TASK - } - ) - } - } - - context.mainActivity?.registerReceiver(onCompleteReceiver, IntentFilter( - DownloadManager.ACTION_DOWNLOAD_COMPLETE - )) - }.show() - } - - return latestVersion - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/MainActivity.kt @@ -0,0 +1,10 @@ +package me.rhunk.snapenhance.manager + +import android.os.Bundle +import androidx.activity.ComponentActivity + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt b/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt @@ -1,41 +0,0 @@ -package me.rhunk.snapenhance.manager.impl - -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.action.AbstractAction -import me.rhunk.snapenhance.action.impl.CheckForUpdates -import me.rhunk.snapenhance.action.impl.CleanCache -import me.rhunk.snapenhance.action.impl.ClearMessageLogger -import me.rhunk.snapenhance.action.impl.ExportChatMessages -import me.rhunk.snapenhance.action.impl.OpenMap -import me.rhunk.snapenhance.action.impl.RefreshMappings -import me.rhunk.snapenhance.manager.Manager -import kotlin.reflect.KClass - -class ActionManager( - private val context: ModContext, -) : Manager { - private val actions = mutableMapOf<String, AbstractAction>() - fun getActions() = actions.values.toList() - private fun load(clazz: KClass<out AbstractAction>) { - val action = clazz.java.newInstance() - action.context = context - actions[action.nameKey] = action - } - override fun init() { - load(CleanCache::class) - load(ExportChatMessages::class) - load(OpenMap::class) - - if(!BuildConfig.DEBUG) { - load(CheckForUpdates::class) - } - else { - load(ClearMessageLogger::class) - load(RefreshMappings::class) - } - - - actions.values.forEach(AbstractAction::init) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt @@ -1,85 +0,0 @@ -package me.rhunk.snapenhance.ui - -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.widget.EditText -import android.widget.TextView -import android.widget.Toast -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.util.ActivityResultCallback -import kotlin.math.abs -import kotlin.random.Random - -class ItemHelper( - private val context : Context -) { - val positiveButtonText by lazy { - SharedContext.translation["button.ok"] - } - - val cancelButtonText by lazy { - SharedContext.translation["button.cancel"] - } - - fun longToast(message: String, context: Context) { - Toast.makeText(context, message, Toast.LENGTH_LONG).show() - } - - fun createTranslatedTextView(property: ConfigProperty, shouldTranslatePropertyValue: Boolean = true): TextView { - return object: TextView(context) { - override fun setText(text: CharSequence?, type: BufferType?) { - val newText = text?.takeIf { it.isNotEmpty() }?.let { - if (!shouldTranslatePropertyValue || property.disableValueLocalization) it - else SharedContext.translation[property.getOptionTranslationKey(it.toString())] - }?.let { - if (it.length > 20) { - "...${it.substring(it.length - 20)}" - } else { - it - } - } ?: "" - super.setTextColor(context.getColor(R.color.tertiaryText)) - super.setText(newText, type) - } - } - } - - fun askForValue(property: ConfigProperty, requestedInputType: Int, callback: (String) -> Unit) { - val editText = EditText(context).apply { - inputType = requestedInputType - setText(property.valueContainer.value().toString()) - } - AlertDialog.Builder(context) - .setTitle(SharedContext.translation["property.${property.translationKey}.name"]) - .setView(editText) - .setPositiveButton(positiveButtonText) { _, _ -> - callback(editText.text.toString()) - } - .setNegativeButton(cancelButtonText) { dialog, _ -> - dialog.cancel() - } - .show() - } - - fun askForFolder(activity: Activity, property: ConfigProperty, callback: (String) -> Unit): Pair<Int, ActivityResultCallback> { - val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) - .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) - .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - - val requestCode = abs(Random.nextInt()) - activity.startActivityForResult(intent, requestCode) - - return requestCode to let@{_, resultCode, data -> - if (resultCode != Activity.RESULT_OK) return@let - val uri = data?.data ?: return@let - val value = uri.toString() - activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) - property.valueContainer.writeFrom(value) - callback(value) - } - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt @@ -1,279 +0,0 @@ -package me.rhunk.snapenhance.ui.config - -import android.app.Activity -import android.app.AlertDialog -import android.content.Intent -import android.content.res.ColorStateList -import android.os.Bundle -import android.text.Html -import android.text.InputType -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.Switch -import android.widget.TextView -import android.widget.Toast -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.config.ConfigCategory -import me.rhunk.snapenhance.config.ConfigProperty -import me.rhunk.snapenhance.config.impl.ConfigIntegerValue -import me.rhunk.snapenhance.config.impl.ConfigStateListValue -import me.rhunk.snapenhance.config.impl.ConfigStateSelection -import me.rhunk.snapenhance.config.impl.ConfigStateValue -import me.rhunk.snapenhance.config.impl.ConfigStringValue -import me.rhunk.snapenhance.ui.ItemHelper -import me.rhunk.snapenhance.util.ActivityResultCallback -import kotlin.system.exitProcess - - -class ConfigActivity : Activity() { - private val itemHelper = ItemHelper(this) - private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>() - - @Deprecated("Deprecated in Java") - @Suppress("DEPRECATION") - override fun onBackPressed() { - super.onBackPressed() - finish() - } - - override fun onDestroy() { - super.onDestroy() - SharedContext.config.writeConfig() - } - - override fun onPause() { - super.onPause() - SharedContext.config.writeConfig() - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - activityResultCallbacks[requestCode]?.invoke(requestCode, resultCode, data) - } - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - SharedContext.ensureInitialized(this) - setContentView(R.layout.config_activity) - - findViewById<View>(R.id.title_bar).let { titleBar -> - titleBar.findViewById<TextView>(R.id.title).text = SharedContext.translation["config_activity.title"] - titleBar.findViewById<ImageButton>(R.id.back_button).visibility = View.GONE - } - - val propertyListLayout = findViewById<ViewGroup>(R.id.property_list) - - if (intent.getBooleanExtra("lspatched", false) || - applicationInfo.packageName != "me.rhunk.snapenhance" || - BuildConfig.DEBUG) { - propertyListLayout.addView( - layoutInflater.inflate( - R.layout.config_activity_debug_item, - propertyListLayout, - false - ).apply { - findViewById<TextView>(R.id.debug_item_content).apply { - text = Html.fromHtml( - "You are using a <u><b>debug/unofficial</b></u> build!\n" + - "Please consider downloading stable builds from <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>.", - Html.FROM_HTML_MODE_COMPACT - ) - movementMethod = android.text.method.LinkMovementMethod.getInstance() - } - }) - } - - //check if save folder is set - //TODO: first run activity - run { - val saveFolder = SharedContext.config.string(ConfigProperty.SAVE_FOLDER) - val itemHelper = ItemHelper(this) - - if (saveFolder.isEmpty() || !saveFolder.startsWith("content://")) { - AlertDialog.Builder(this) - .setTitle("Save folder") - .setMessage("Please select a folder where you want to save downloaded files.") - .setPositiveButton("Select") { _, _ -> - val (requestCode, callback) = itemHelper.askForFolder( - this, - ConfigProperty.SAVE_FOLDER - ) {} - activityResultCallbacks[requestCode] = { a1, a2, a3 -> - callback(a1, a2, a3) - Toast.makeText(this, "Save Folder set!", Toast.LENGTH_SHORT).show() - finish() - } - } - .setNegativeButton("Cancel") { _, _ -> - exitProcess(0) - } - .show() - } - } - - var currentCategory: ConfigCategory? = null - - SharedContext.config.entries().filter { !it.key.category.hidden }.forEach { (property, value) -> - val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) - - fun addSeparator() { - //add separator - propertyListLayout.addView(View(this).apply { - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) - setBackgroundColor(getColor(R.color.tertiaryBackground)) - }) - } - - if (property.category != currentCategory) { - if(!property.shouldAppearInSettings) return@forEach - currentCategory = property.category - with(layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)) { - findViewById<TextView>(R.id.name).apply { - text = SharedContext.translation["category.${property.category.key}"] - textSize = 20f - typeface = typeface?.let { android.graphics.Typeface.create(it, android.graphics.Typeface.BOLD) } - } - propertyListLayout.addView(this) - } - addSeparator() - } - - if (!property.shouldAppearInSettings) return@forEach - - val propertyName = SharedContext.translation["property.${property.translationKey}.name"] - - configItem.findViewById<TextView>(R.id.name).text = propertyName - configItem.findViewById<TextView>(R.id.description).also { - it.text = SharedContext.translation["property.${property.translationKey}.description"] - it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE - } - - fun addValueView(view: View) { - configItem.findViewById<ViewGroup>(R.id.value).addView(view) - } - - when (value) { - is ConfigStateValue -> { - val switch = Switch(this) - switch.isChecked = value.value() - switch.trackTintList = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_checked), - intArrayOf(-android.R.attr.state_checked) - ), - intArrayOf( - switch.highlightColor, - getColor(R.color.tertiaryBackground) - ) - ) - switch.setOnCheckedChangeListener { _, isChecked -> - value.writeFrom(isChecked.toString()) - } - configItem.setOnClickListener { switch.toggle() } - addValueView(switch) - } - is ConfigStringValue, is ConfigIntegerValue -> { - val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { - it.text = value.value().toString() - } - configItem.setOnClickListener { - if (value is ConfigStringValue && value.isFolderPath) { - val (requestCode, callback) = itemHelper.askForFolder(this, property) { - value.writeFrom(it) - textView.text = value.value() - } - - activityResultCallbacks[requestCode] = callback - return@setOnClickListener - } - - if (value is ConfigIntegerValue) { - itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) { - try { - value.writeFrom(it) - textView.text = value.value().toString() - } catch (e: NumberFormatException) { - itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this) - } - } - return@setOnClickListener - } - itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) { - value.writeFrom(it) - textView.text = value.value().toString() - } - } - addValueView(textView) - } - is ConfigStateListValue -> { - val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false) - val values = value.value() - - fun updateText() { - textView.text = SharedContext.translation.format("config_activity.selected_text", "count" to values.filter { it.value }.size.toString()) - } - - updateText() - - configItem.setOnClickListener { - AlertDialog.Builder(this) - .setTitle(propertyName) - .setPositiveButton(itemHelper.positiveButtonText) { _, _ -> - updateText() - } - .setMultiChoiceItems( - values.keys.map { - if (property.disableValueLocalization) it - else SharedContext.translation[property.getOptionTranslationKey(it)] - }.toTypedArray(), - values.map { it.value }.toBooleanArray() - ) { _, which, isChecked -> - value.setKey(values.keys.elementAt(which), isChecked) - } - .show() - } - - addValueView(textView) - } - is ConfigStateSelection -> { - val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = true) - textView.text = value.value() - - configItem.setOnClickListener { - val builder = AlertDialog.Builder(this) - builder.setTitle(propertyName) - - builder.setSingleChoiceItems( - value.keys().toTypedArray().map { - if (property.disableValueLocalization) it - else SharedContext.translation[property.getOptionTranslationKey(it)] - }.toTypedArray(), - value.keys().indexOf(value.value()) - ) { _, which -> - value.writeFrom(value.keys()[which]) - } - - builder.setPositiveButton(itemHelper.positiveButtonText) { _, _ -> - textView.text = value.value() - } - - builder.show() - } - addValueView(textView) - } - } - - propertyListLayout.addView(configItem) - addSeparator() - } - - propertyListLayout.addView(layoutInflater.inflate(R.layout.config_activity_debug_item, propertyListLayout, false).apply { - findViewById<TextView>(R.id.debug_item_content).apply { - text = Html.fromHtml("Made by rhunk on <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>", Html.FROM_HTML_MODE_COMPACT) - movementMethod = android.text.method.LinkMovementMethod.getInstance() - } - }) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt @@ -1,113 +0,0 @@ -package me.rhunk.snapenhance.ui.download - -import android.app.AlertDialog -import android.content.Intent -import android.view.View -import android.view.ViewGroup -import android.widget.ArrayAdapter -import android.widget.ImageButton -import android.widget.ListView -import android.widget.TextView -import android.widget.Toast -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.bridge.types.BridgeFileType -import me.rhunk.snapenhance.ui.config.ConfigActivity -import me.rhunk.snapenhance.ui.spoof.DeviceSpooferActivity -import java.io.File - -class ActionListAdapter( - private val activity: DownloadManagerActivity, - private val layoutId: Int, - private val actions: Array<Pair<String, () -> Unit>> -) : ArrayAdapter<Pair<String, () -> Unit>>(activity, layoutId, actions) { - override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { - val view = convertView ?: activity.layoutInflater.inflate(layoutId, parent, false) - val action = actions[position] - view.isClickable = true - - view.findViewById<TextView>(R.id.feature_text).text = action.first - view.setOnClickListener { - action.second() - } - - return view - } -} - -class DebugSettingsLayoutInflater( - private val activity: DownloadManagerActivity -) { - private fun confirmAction(title: String, message: String, action: () -> Unit) { - activity.runOnUiThread { - AlertDialog.Builder(activity) - .setTitle(title) - .setMessage(message) - .setPositiveButton(SharedContext.translation["button.positive"]) { _, _ -> - action() - } - .setNegativeButton(SharedContext.translation["button.negative"]) { _, _ -> } - .show() - } - } - - private fun showSuccessToast() { - Toast.makeText(activity, "Success", Toast.LENGTH_SHORT).show() - } - - fun inflate(parent: ViewGroup) { - val debugSettingsLayout = activity.layoutInflater.inflate(R.layout.debug_settings_page, parent, false) - - val debugSettingsTranslation = activity.translation.getCategory("debug_settings_page") - - debugSettingsLayout.findViewById<ImageButton>(R.id.back_button).setOnClickListener { - parent.removeView(debugSettingsLayout) - } - - debugSettingsLayout.findViewById<TextView>(R.id.title).text = activity.translation["debug_settings"] - - debugSettingsLayout.findViewById<ListView>(R.id.setting_page_list).apply { - adapter = ActionListAdapter(activity, R.layout.debug_setting_item, mutableListOf<Pair<String, () -> Unit>>().apply { - add(SharedContext.translation["config_activity.title"] to { - activity.startActivity(Intent(activity, ConfigActivity::class.java)) - }) - add(SharedContext.translation["spoof_activity.title"] to { - activity.startActivity(Intent(activity, DeviceSpooferActivity::class.java)) - }) - add(debugSettingsTranslation["clear_cache_title"] to { - context.cacheDir.listFiles()?.forEach { - it.deleteRecursively() - } - showSuccessToast() - }) - - BridgeFileType.values().forEach { fileType -> - val actionName = debugSettingsTranslation.format("clear_file_title", "file_name" to fileType.displayName) - add(actionName to { - confirmAction(actionName, debugSettingsTranslation.format("clear_file_confirmation", "file_name" to fileType.displayName)) { - fileType.resolve(context).deleteRecursively() - showSuccessToast() - } - }) - } - - add(debugSettingsTranslation["reset_all_title"] to { - confirmAction(debugSettingsTranslation["reset_all_title"], debugSettingsTranslation["reset_all_confirmation"]) { - arrayOf(context.cacheDir, context.filesDir, File(context.dataDir, "databases"), File(context.dataDir, "shared_prefs")).forEach { - it.listFiles()?.forEach { file -> - file.deleteRecursively() - } - } - showSuccessToast() - } - }) - }.toTypedArray()) - } - - activity.registerBackCallback { - parent.removeView(debugSettingsLayout) - } - - parent.addView(debugSettingsLayout) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt @@ -1,242 +0,0 @@ -package me.rhunk.snapenhance.ui.download - -import android.annotation.SuppressLint -import android.content.Intent -import android.graphics.Bitmap -import android.graphics.BitmapFactory -import android.net.Uri -import android.os.Handler -import android.view.LayoutInflater -import android.view.View -import android.view.ViewGroup -import android.widget.Button -import android.widget.ImageView -import android.widget.TextView -import android.widget.Toast -import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory -import androidx.recyclerview.widget.RecyclerView -import androidx.recyclerview.widget.RecyclerView.Adapter -import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.Job -import kotlinx.coroutines.job -import kotlinx.coroutines.launch -import kotlinx.coroutines.withTimeout -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.download.data.PendingDownload -import me.rhunk.snapenhance.download.enums.DownloadStage -import me.rhunk.snapenhance.util.snap.PreviewUtils -import java.io.File -import java.io.FileInputStream -import java.net.URL -import kotlin.concurrent.thread -import kotlin.coroutines.coroutineContext - -class DownloadListAdapter( - private val activity: DownloadManagerActivity, - private val downloadList: MutableList<PendingDownload> -): Adapter<DownloadListAdapter.ViewHolder>() { - private val coroutineScope = CoroutineScope(Dispatchers.IO) - private val previewJobs = mutableMapOf<Int, Job>() - - inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) { - val bitmojiIcon: ImageView = view.findViewById(R.id.bitmoji_icon) - val title: TextView = view.findViewById(R.id.item_title) - val subtitle: TextView = view.findViewById(R.id.item_subtitle) - val status: TextView = view.findViewById(R.id.item_status) - val actionButton: Button = view.findViewById(R.id.item_action_button) - val radius by lazy { - view.context.resources.getDimensionPixelSize(R.dimen.download_manager_item_preview_radius) - } - val viewWidth by lazy { - view.resources.displayMetrics.widthPixels - } - val viewHeight by lazy { - view.layoutParams.height - } - } - - override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { - return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.download_manager_item, parent, false)) - } - - override fun getItemCount(): Int { - return downloadList.size - } - - @SuppressLint("Recycle") - private suspend fun handlePreview(download: PendingDownload, holder: ViewHolder) { - download.outputFile?.let { - val uri = Uri.parse(it) - runCatching { - if (uri.scheme == "content") { - val fileType = activity.contentResolver.openInputStream(uri)!!.use { stream -> - FileType.fromInputStream(stream) - } - fileType to activity.contentResolver.openInputStream(uri) - } else { - FileType.fromFile(File(it)) to FileInputStream(it) - } - }.getOrNull() - }?.also { (fileType, assetStream) -> - val previewBitmap = assetStream?.use { stream -> - //don't preview files larger than 30MB - if (stream.available() > 30 * 1024 * 1024) return@also - - val tempFile = File.createTempFile("preview", ".${fileType.fileExtension}") - tempFile.outputStream().use { output -> - stream.copyTo(output) - } - runCatching { - PreviewUtils.createPreviewFromFile(tempFile)?.let { preview -> - val offsetY = (preview.height / 2 - holder.viewHeight / 2).coerceAtLeast(0) - - Bitmap.createScaledBitmap( - Bitmap.createBitmap( - preview, 0, offsetY, - preview.width.coerceAtMost(holder.viewWidth), - preview.height.coerceAtMost(holder.viewHeight) - ), - holder.viewWidth, - holder.viewHeight, - false - ) - } - }.onFailure { - Logger.error("failed to create preview $fileType", it) - }.also { - tempFile.delete() - }.getOrNull() - } ?: return@also - - if (coroutineContext.job.isCancelled) return@also - Handler(holder.view.context.mainLooper).post { - holder.view.background = RoundedBitmapDrawableFactory.create( - holder.view.context.resources, - previewBitmap - ).also { - it.cornerRadius = holder.radius.toFloat() - } - } - } - } - - private fun updateViewHolder(download: PendingDownload, holder: ViewHolder) { - holder.status.text = download.downloadStage.toString() - holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background) - - coroutineScope.launch { - withTimeout(2000) { - handlePreview(download, holder) - } - } - - val isSaved = download.downloadStage == DownloadStage.SAVED - //if the download is in progress, the user can cancel it - val canInteract = if (download.job != null) !download.downloadStage.isFinalStage || isSaved - else isSaved - - holder.status.visibility = if (isSaved) View.GONE else View.VISIBLE - - with(holder.actionButton) { - isEnabled = canInteract - alpha = if (canInteract) 1f else 0.5f - background = context.getDrawable(if (isSaved) R.drawable.action_button_success else R.drawable.action_button_cancel) - setTextColor(context.getColor(if (isSaved) R.color.successColor else R.color.actionBarColor)) - text = if (isSaved) - SharedContext.translation["button.open"] - else - SharedContext.translation["button.cancel"] - } - } - - override fun onBindViewHolder(holder: ViewHolder, position: Int) { - val pendingDownload = downloadList[position] - - pendingDownload.changeListener = { _, _ -> - Handler(holder.view.context.mainLooper).post { - updateViewHolder(pendingDownload, holder) - notifyItemChanged(position) - } - } - - holder.bitmojiIcon.setImageResource(R.drawable.bitmoji_blank) - - pendingDownload.metadata.iconUrl?.let { url -> - thread(start = true) { - runCatching { - val iconBitmap = URL(url).openStream().use { - BitmapFactory.decodeStream(it) - } - Handler(holder.view.context.mainLooper).post { - holder.bitmojiIcon.setImageBitmap(iconBitmap) - } - } - } - } - - holder.title.visibility = View.GONE - holder.subtitle.visibility = View.GONE - - pendingDownload.metadata.mediaDisplayType?.let { - holder.title.text = it - holder.title.visibility = View.VISIBLE - } - - pendingDownload.metadata.mediaDisplaySource?.let { - holder.subtitle.text = it - holder.subtitle.visibility = View.VISIBLE - } - - holder.actionButton.setOnClickListener { - if (pendingDownload.downloadStage != DownloadStage.SAVED) { - pendingDownload.cancel() - pendingDownload.downloadStage = DownloadStage.CANCELLED - updateViewHolder(pendingDownload, holder) - notifyItemChanged(position); - return@setOnClickListener - } - - pendingDownload.outputFile?.let { - fun showFileNotFound() { - Toast.makeText(holder.view.context, SharedContext.translation["download_manager_activity.file_not_found_toast"], Toast.LENGTH_SHORT).show() - } - - val uri = Uri.parse(it) - val fileType = runCatching { - if (uri.scheme == "content") { - activity.contentResolver.openInputStream(uri)?.use { input -> - FileType.fromInputStream(input) - } ?: run { - showFileNotFound() - return@setOnClickListener - } - } else { - val file = File(it) - if (!file.exists()) { - showFileNotFound() - return@setOnClickListener - } - FileType.fromFile(file) - } - }.onFailure { exception -> - Logger.error("Failed to open file", exception) - }.getOrDefault(FileType.UNKNOWN) - if (fileType == FileType.UNKNOWN) { - showFileNotFound() - return@setOnClickListener - } - - val intent = Intent(Intent.ACTION_VIEW) - intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION - intent.setDataAndType(uri, fileType.mimeType) - holder.view.context.startActivity(intent) - } - } - - updateViewHolder(pendingDownload, holder) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt @@ -1,214 +0,0 @@ -package me.rhunk.snapenhance.ui.download - -import android.annotation.SuppressLint -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Bundle -import android.os.PowerManager -import android.provider.Settings -import android.view.View -import android.widget.Button -import android.widget.ImageButton -import android.widget.TextView -import androidx.recyclerview.widget.ItemTouchHelper -import androidx.recyclerview.widget.RecyclerView -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper -import me.rhunk.snapenhance.download.data.PendingDownload - -class DownloadManagerActivity : Activity() { - lateinit var translation: TranslationWrapper - - private val backCallbacks = mutableListOf<() -> Unit>() - private val fetchedDownloadTasks = mutableListOf<PendingDownload>() - private var listFilter = MediaFilter.NONE - - private val preferences by lazy { - getSharedPreferences("settings", Context.MODE_PRIVATE) - } - - private fun updateNoDownloadText() { - findViewById<TextView>(R.id.no_download_title).let { - it.text = translation["no_downloads"] - it.visibility = if (fetchedDownloadTasks.isEmpty()) View.VISIBLE else View.GONE - } - } - - @SuppressLint("NotifyDataSetChanged") - private fun updateListContent() { - fetchedDownloadTasks.clear() - fetchedDownloadTasks.addAll(SharedContext.downloadTaskManager.queryAllTasks(filter = listFilter).values) - - with(findViewById<RecyclerView>(R.id.download_list)) { - adapter?.notifyDataSetChanged() - scrollToPosition(0) - } - updateNoDownloadText() - } - - @Deprecated("Deprecated in Java") - @Suppress("DEPRECATION") - override fun onBackPressed() { - backCallbacks.lastOrNull()?.let { - it() - backCallbacks.removeLast() - } ?: super.onBackPressed() - } - - fun registerBackCallback(callback: () -> Unit) { - backCallbacks.add(callback) - } - - @SuppressLint("BatteryLife", "NotifyDataSetChanged", "SetTextI18n") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - SharedContext.ensureInitialized(this) - translation = SharedContext.translation.getCategory("download_manager_activity") - - setContentView(R.layout.download_manager_activity) - - findViewById<TextView>(R.id.title).text = resources.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME - - findViewById<ImageButton>(R.id.debug_settings_button).setOnClickListener { - DebugSettingsLayoutInflater(this).inflate(findViewById(android.R.id.content)) - } - - with(findViewById<RecyclerView>(R.id.download_list)) { - layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this@DownloadManagerActivity) - - adapter = DownloadListAdapter(this@DownloadManagerActivity, fetchedDownloadTasks).apply { - registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { - override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { - updateNoDownloadText() - } - }) - } - - ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { - override fun getMovementFlags( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder - ): Int { - val download = fetchedDownloadTasks[viewHolder.absoluteAdapterPosition] - return if (download.isJobActive()) { - 0 - } else { - super.getMovementFlags(recyclerView, viewHolder) - } - } - - override fun onMove( - recyclerView: RecyclerView, - viewHolder: RecyclerView.ViewHolder, - target: RecyclerView.ViewHolder - ): Boolean { - return false - } - - @SuppressLint("NotifyDataSetChanged") - override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { - fetchedDownloadTasks.removeAt(viewHolder.absoluteAdapterPosition).let { - SharedContext.downloadTaskManager.removeTask(it) - } - adapter?.notifyItemRemoved(viewHolder.absoluteAdapterPosition) - } - }).attachToRecyclerView(this) - - var isLoading = false - - addOnScrollListener(object : RecyclerView.OnScrollListener() { - override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { - val layoutManager = recyclerView.layoutManager as androidx.recyclerview.widget.LinearLayoutManager - val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() - - if (lastVisibleItemPosition == RecyclerView.NO_POSITION) { - return - } - - if (lastVisibleItemPosition == fetchedDownloadTasks.size - 1 && !isLoading) { - isLoading = true - - SharedContext.downloadTaskManager.queryTasks(fetchedDownloadTasks.last().downloadId, filter = listFilter).forEach { - fetchedDownloadTasks.add(it.value) - adapter?.notifyItemInserted(fetchedDownloadTasks.size - 1) - } - - isLoading = false - } - } - }) - - arrayOf( - Pair(R.id.all_category, MediaFilter.NONE), - Pair(R.id.pending_category, MediaFilter.PENDING), - Pair(R.id.snap_category, MediaFilter.CHAT_MEDIA), - Pair(R.id.story_category, MediaFilter.STORY), - Pair(R.id.spotlight_category, MediaFilter.SPOTLIGHT) - ).let { categoryPairs -> - categoryPairs.forEach { pair -> - this@DownloadManagerActivity.findViewById<TextView>(pair.first).apply { - text = translation["category.${resources.getResourceEntryName(pair.first)}"] - }.setOnClickListener { view -> - listFilter = pair.second - updateListContent() - categoryPairs.map { this@DownloadManagerActivity.findViewById<TextView>(it.first) }.forEach { - it.setTextColor(getColor(R.color.primaryText)) - } - (view as TextView).setTextColor(getColor(R.color.focusedCategoryColor)) - } - } - } - - this@DownloadManagerActivity.findViewById<Button>(R.id.remove_all_button).also { - it.text = translation["remove_all"] - }.setOnClickListener { - with(AlertDialog.Builder(this@DownloadManagerActivity)) { - setTitle(translation["remove_all_title"]) - setMessage(translation["remove_all_text"]) - setPositiveButton(SharedContext.translation["button.positive"]) { _, _ -> - SharedContext.downloadTaskManager.removeAllTasks() - fetchedDownloadTasks.removeIf { - if (it.isJobActive()) it.cancel() - true - } - adapter?.notifyDataSetChanged() - updateNoDownloadText() - } - setNegativeButton(SharedContext.translation["button.negative"]) { dialog, _ -> - dialog.dismiss() - } - show() - } - } - - } - - updateListContent() - - if (!preferences.getBoolean("ask_battery_optimisations", true) || - !(getSystemService(Context.POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName)) return - - with(Intent()) { - action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - data = Uri.parse("package:$packageName") - startActivityForResult(this, 1) - } - } - - override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { - if (requestCode == 1) { - preferences.edit().putBoolean("ask_battery_optimisations", false).apply() - } - } - - @SuppressLint("NotifyDataSetChanged") - override fun onResume() { - super.onResume() - updateListContent() - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/map/MapActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/map/MapActivity.kt @@ -1,98 +0,0 @@ -package me.rhunk.snapenhance.ui.map - -import android.annotation.SuppressLint -import android.app.Activity -import android.app.AlertDialog -import android.content.Context -import android.os.Bundle -import android.view.MotionEvent -import android.widget.Button -import android.widget.EditText -import me.rhunk.snapenhance.R -import org.osmdroid.config.Configuration -import org.osmdroid.tileprovider.tilesource.TileSourceFactory -import org.osmdroid.util.GeoPoint -import org.osmdroid.views.MapView -import org.osmdroid.views.Projection -import org.osmdroid.views.overlay.Marker -import org.osmdroid.views.overlay.Overlay - - -class MapActivity : Activity() { - - private lateinit var mapView: MapView - - @SuppressLint("MissingInflatedId", "ResourceType") - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - - val contextBundle = intent.extras?.getBundle("location") ?: return - val locationLatitude = contextBundle.getDouble("latitude") - val locationLongitude = contextBundle.getDouble("longitude") - - Configuration.getInstance().load(applicationContext, getSharedPreferences("osmdroid", Context.MODE_PRIVATE)) - - setContentView(R.layout.map) - - mapView = findViewById(R.id.mapView) - mapView.setMultiTouchControls(true); - mapView.setTileSource(TileSourceFactory.MAPNIK) - - val startPoint = GeoPoint(locationLatitude, locationLongitude) - mapView.controller.setZoom(10.0) - mapView.controller.setCenter(startPoint) - - val marker = Marker(mapView) - marker.isDraggable = true - marker.position = startPoint - marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) - - mapView.overlays.add(object: Overlay() { - override fun onSingleTapConfirmed(e: MotionEvent?, mapView: MapView?): Boolean { - val proj: Projection = mapView!!.projection - val loc = proj.fromPixels(e!!.x.toInt(), e.y.toInt()) as GeoPoint - marker.position = loc - mapView.invalidate() - return true - } - }) - - mapView.overlays.add(marker) - - val applyButton = findViewById<Button>(R.id.apply_location_button) - applyButton.setOnClickListener { - val bundle = Bundle() - bundle.putFloat("latitude", marker.position.latitude.toFloat()) - bundle.putFloat("longitude", marker.position.longitude.toFloat()) - setResult(RESULT_OK, intent.putExtra("location", bundle)) - finish() - } - - val setPreciseLocationButton = findViewById<Button>(R.id.set_precise_location_button) - - setPreciseLocationButton.setOnClickListener { - val locationDialog = layoutInflater.inflate(R.layout.precise_location_dialog, null) - val dialogLatitude = locationDialog.findViewById<EditText>(R.id.dialog_latitude).also { it.setText(marker.position.latitude.toString()) } - val dialogLongitude = locationDialog.findViewById<EditText>(R.id.dialog_longitude).also { it.setText(marker.position.longitude.toString()) } - - AlertDialog.Builder(this) - .setView(locationDialog) - .setTitle("Set a precise location") - .setPositiveButton("Set") { _, _ -> - val latitude = dialogLatitude.text.toString().toDoubleOrNull() - val longitude = dialogLongitude.text.toString().toDoubleOrNull() - if (latitude != null && longitude != null) { - val preciseLocation = GeoPoint(latitude, longitude) - mapView.controller.setCenter(preciseLocation) - marker.position = preciseLocation - mapView.invalidate() - } - }.setNegativeButton("Cancel") { _, _ -> }.show() - } - } - - override fun onDestroy() { - super.onDestroy() - mapView.onDetach() - } -} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt @@ -1,86 +0,0 @@ -package me.rhunk.snapenhance.ui.menu.impl - -import android.annotation.SuppressLint -import android.content.Intent -import android.view.View -import android.view.ViewGroup -import android.widget.FrameLayout -import android.widget.ImageView -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.Constants -import me.rhunk.snapenhance.ui.config.ConfigActivity -import me.rhunk.snapenhance.ui.menu.AbstractMenu -import java.io.File - - -@SuppressLint("DiscouragedApi") -class SettingsGearInjector : AbstractMenu() { - private val headerButtonOpaqueIconTint by lazy { - context.resources.getIdentifier("headerButtonOpaqueIconTint", "attr", Constants.SNAPCHAT_PACKAGE_NAME).let { - context.androidContext.theme.obtainStyledAttributes(intArrayOf(it)).getColorStateList(0) - } - } - - private val settingsSvg by lazy { - context.resources.getIdentifier("svg_settings_32x32", "drawable", Constants.SNAPCHAT_PACKAGE_NAME).let { - context.resources.getDrawable(it, context.androidContext.theme) - } - } - - private val ngsHovaHeaderSearchIconBackgroundMarginLeft by lazy { - context.resources.getIdentifier("ngs_hova_header_search_icon_background_margin_left", "dimen", Constants.SNAPCHAT_PACKAGE_NAME).let { - context.resources.getDimensionPixelSize(it) - } - } - - @SuppressLint("SetTextI18n", "ClickableViewAccessibility") - fun inject(parent: ViewGroup, child: View) { - val firstView = (child as ViewGroup).getChildAt(0) - - child.clipChildren = false - child.addView(FrameLayout(parent.context).apply { - layoutParams = FrameLayout.LayoutParams(firstView.layoutParams.width, firstView.layoutParams.height).apply { - y = 0f - x = -(ngsHovaHeaderSearchIconBackgroundMarginLeft + firstView.layoutParams.width).toFloat() - } - - isClickable = true - - setOnClickListener { - val intent = Intent().apply { - setClassName(BuildConfig.APPLICATION_ID, ConfigActivity::class.java.name) - } - intent.putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists()) - context.startActivity(intent) - } - - parent.setOnTouchListener { _, event -> - if (child.visibility == View.INVISIBLE || child.alpha == 0F) return@setOnTouchListener false - - val viewLocation = IntArray(2) - getLocationOnScreen(viewLocation) - - val x = event.rawX - viewLocation[0] - val y = event.rawY - viewLocation[1] - - if (x > 0 && x < width && y > 0 && y < height) { - performClick() - } - - false - } - backgroundTintList = firstView.backgroundTintList - background = firstView.background - - addView(ImageView(context).apply { - layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 17).apply { - gravity = android.view.Gravity.CENTER - } - setImageDrawable(settingsSvg) - headerButtonOpaqueIconTint?.let { - imageTintList = it - } - }) - }) - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt b/app/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt @@ -1,111 +0,0 @@ -package me.rhunk.snapenhance.ui.spoof - -import android.app.Activity -import android.content.res.ColorStateList -import android.os.Bundle -import android.text.InputType -import android.view.View -import android.view.ViewGroup -import android.widget.ImageButton -import android.widget.Switch -import android.widget.TextView -import me.rhunk.snapenhance.R -import me.rhunk.snapenhance.SharedContext -import me.rhunk.snapenhance.config.ConfigCategory -import me.rhunk.snapenhance.config.impl.ConfigIntegerValue -import me.rhunk.snapenhance.config.impl.ConfigStateValue -import me.rhunk.snapenhance.config.impl.ConfigStringValue -import me.rhunk.snapenhance.ui.ItemHelper - -class DeviceSpooferActivity: Activity() { - private val itemHelper = ItemHelper(this) - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - SharedContext.ensureInitialized(this) - setContentView(R.layout.device_spoofer_activity) - - findViewById<TextView>(R.id.title).text = "Device Spoofer" - findViewById<ImageButton>(R.id.back_button).setOnClickListener { finish() } - val propertyListLayout = findViewById<ViewGroup>(R.id.spoof_property_list) - - SharedContext.config.entries().filter { it.key.category == ConfigCategory.DEVICE_SPOOFER }.forEach { (property, value) -> - val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) - - val propertyName = SharedContext.translation["property.${property.translationKey}.name"] - - fun addSeparator() { - //add separator - propertyListLayout.addView(View(this).apply { - layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) - setBackgroundColor(getColor(R.color.tertiaryBackground)) - }) - } - - configItem.findViewById<TextView>(R.id.name).text = propertyName - configItem.findViewById<TextView>(R.id.description).also { - it.text = SharedContext.translation["property.${property.translationKey}.description"] - it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE - } - - fun addValueView(view: View) { - configItem.findViewById<ViewGroup>(R.id.value).addView(view) - } - - when (value) { - is ConfigStateValue -> { - val switch = Switch(this) - switch.isChecked = value.value() - switch.trackTintList = ColorStateList( - arrayOf( - intArrayOf(android.R.attr.state_checked), - intArrayOf(-android.R.attr.state_checked) - ), - intArrayOf( - switch.highlightColor, - getColor(R.color.tertiaryBackground) - ) - ) - switch.setOnCheckedChangeListener { _, isChecked -> - value.writeFrom(isChecked.toString()) - } - configItem.setOnClickListener { switch.toggle() } - addValueView(switch) - } - is ConfigStringValue, is ConfigIntegerValue -> { - val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { - it.text = value.value().toString() - } - configItem.setOnClickListener { - if (value is ConfigIntegerValue) { - itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) { - try { - value.writeFrom(it) - textView.text = value.value().toString() - } catch (e: NumberFormatException) { - itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this) - } - } - return@setOnClickListener - } - itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) { - value.writeFrom(it) - textView.text = value.value().toString() - } - } - addValueView(textView) - } - } - - propertyListLayout.addView(configItem) - addSeparator() - } - } - - @Deprecated("Deprecated in Java") - @Suppress("DEPRECATION") - override fun onBackPressed() { - super.onBackPressed() - finish() - } -}- \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt b/app/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt @@ -1,337 +0,0 @@ -package me.rhunk.snapenhance.util.export - -import android.content.pm.PackageManager -import android.os.Environment -import android.util.Base64InputStream -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import de.robv.android.xposed.XposedHelpers -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.async -import kotlinx.coroutines.awaitAll -import kotlinx.coroutines.withContext -import me.rhunk.snapenhance.BuildConfig -import me.rhunk.snapenhance.Logger -import me.rhunk.snapenhance.ModContext -import me.rhunk.snapenhance.data.ContentType -import me.rhunk.snapenhance.data.FileType -import me.rhunk.snapenhance.data.MediaReferenceType -import me.rhunk.snapenhance.data.wrapper.impl.Message -import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID -import me.rhunk.snapenhance.database.objects.FriendFeedInfo -import me.rhunk.snapenhance.database.objects.FriendInfo -import me.rhunk.snapenhance.util.getApplicationInfoCompat -import me.rhunk.snapenhance.util.protobuf.ProtoReader -import me.rhunk.snapenhance.util.snap.EncryptionHelper -import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper -import java.io.File -import java.io.FileOutputStream -import java.io.InputStream -import java.io.OutputStream -import java.text.SimpleDateFormat -import java.util.Base64 -import java.util.Collections -import java.util.Date -import java.util.Locale -import java.util.zip.Deflater -import java.util.zip.DeflaterInputStream -import java.util.zip.ZipFile -import kotlin.io.encoding.ExperimentalEncodingApi - - -enum class ExportFormat( - val extension: String, -){ - JSON("json"), - TEXT("txt"), - HTML("html"); -} - -class MessageExporter( - private val context: ModContext, - private val outputFile: File, - private val friendFeedInfo: FriendFeedInfo, - private val mediaToDownload: List<ContentType>? = null, - private val printLog: (String) -> Unit = {}, -) { - private lateinit var conversationParticipants: Map<String, FriendInfo> - private lateinit var messages: List<Message> - - fun readMessages(messages: List<Message>) { - conversationParticipants = - context.database.getConversationParticipants(friendFeedInfo.key!!) - ?.mapNotNull { - context.database.getFriendInfo(it) - }?.associateBy { it.userId!! } ?: emptyMap() - - if (conversationParticipants.isEmpty()) - throw Throwable("Failed to get conversation participants for ${friendFeedInfo.key}") - - this.messages = messages.sortedBy { it.orderKey } - } - - private fun serializeMessageContent(message: Message): String? { - return if (message.messageContent.contentType == ContentType.CHAT) { - ProtoReader(message.messageContent.content).getString(2, 1) ?: "Failed to parse message" - } else null - } - - private fun exportText(output: OutputStream) { - val writer = output.bufferedWriter() - writer.write("Conversation key: ${friendFeedInfo.key}\n") - writer.write("Conversation Name: ${friendFeedInfo.feedDisplayName}\n") - writer.write("Participants:\n") - conversationParticipants.forEach { (userId, friendInfo) -> - writer.write(" $userId: ${friendInfo.displayName}\n") - } - - writer.write("\nMessages:\n") - messages.forEach { message -> - val sender = conversationParticipants[message.senderId.toString()] - val senderUsername = sender?.usernameForSorting ?: message.senderId.toString() - val senderDisplayName = sender?.displayName ?: message.senderId.toString() - val messageContent = serializeMessageContent(message) ?: message.messageContent.contentType.name - val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).format(Date(message.messageMetadata.createdAt)) - writer.write("[$date] - $senderDisplayName (${senderUsername}): $messageContent\n") - } - writer.flush() - } - - @OptIn(ExperimentalEncodingApi::class) - suspend fun exportHtml(output: OutputStream) { - val downloadMediaCacheFolder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "SnapEnhance/cache").also { it.mkdirs() } - val mediaFiles = Collections.synchronizedMap(mutableMapOf<String, Pair<FileType, File>>()) - - printLog("found ${messages.size} messages") - - withContext(Dispatchers.IO) { - messages.filter { - mediaToDownload?.contains(it.messageContent.contentType) ?: false - }.map { message -> - async { - val remoteMediaReferences by lazy { - val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject - serializedMessageContent["mRemoteMediaReferences"] - .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } - .flatten() - } - - remoteMediaReferences.firstOrNull().takeIf { it != null }?.let { media -> - val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() - - runCatching { - val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference) { - EncryptionHelper.decryptInputStream(it, message.messageContent.contentType, ProtoReader(message.messageContent.content), isArroyo = false) - } - - printLog("downloaded media ${message.orderKey}") - - downloadedMedia.forEach { (type, mediaData) -> - val fileType = FileType.fromByteArray(mediaData) - val fileName = "${type}_${kotlin.io.encoding.Base64.UrlSafe.encode(protoMediaReference).replace("=", "")}" - - val mediaFile = File(downloadMediaCacheFolder, "$fileName.${fileType.fileExtension}") - - FileOutputStream(mediaFile).use { fos -> - mediaData.inputStream().copyTo(fos) - } - - mediaFiles[fileName] = fileType to mediaFile - } - }.onFailure { - printLog("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}") - Logger.error("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}", it) - } - } - } - }.awaitAll() - } - - printLog("writing downloaded medias...") - - //write the head of the html file - output.write(""" - <!DOCTYPE html> - <html> - <head> - <meta charset="UTF-8"> - <meta http-equiv="X-UA-Compatible" content="IE=edge"> - <meta name="viewport" content="width=device-width, initial-scale=1.0"> - <title></title> - </head> - """.trimIndent().toByteArray()) - - output.write("<!-- This file was generated by SnapEnhance ${BuildConfig.VERSION_NAME} -->\n".toByteArray()) - - mediaFiles.forEach { (key, filePair) -> - printLog("writing $key...") - output.write("<div class=\"media-$key\"><!-- ".toByteArray()) - - val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true)) - val base64InputStream = XposedHelpers.newInstance( - Base64InputStream::class.java, - deflateInputStream, - android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, - true - ) as InputStream - base64InputStream.copyTo(output) - deflateInputStream.close() - - output.write(" --></div>\n".toByteArray()) - output.flush() - } - printLog("writing json conversation data...") - - //write the json file - output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray()) - exportJson(output) - output.write("</script>\n".toByteArray()) - - printLog("writing template...") - - runCatching { - ZipFile( - context.androidContext.packageManager.getApplicationInfoCompat(BuildConfig.APPLICATION_ID, PackageManager.GET_META_DATA).publicSourceDir - ).use { apkFile -> - //export rawinflate.js - apkFile.getEntry("assets/web/rawinflate.js").let { entry -> - output.write("<script>".toByteArray()) - apkFile.getInputStream(entry).copyTo(output) - output.write("</script>\n".toByteArray()) - } - - //export avenir next font - apkFile.getEntry("res/font/avenir_next_medium.ttf").let { entry -> - val encodedFontData = kotlin.io.encoding.Base64.Default.encode(apkFile.getInputStream(entry).readBytes()) - output.write(""" - <style> - @font-face { - font-family: 'Avenir Next'; - src: url('data:font/truetype;charset=utf-8;base64, $encodedFontData'); - font-weight: normal; - font-style: normal; - } - </style> - """.trimIndent().toByteArray()) - } - - apkFile.getEntry("assets/web/export_template.html").let { entry -> - apkFile.getInputStream(entry).copyTo(output) - } - - apkFile.close() - } - }.onFailure { - printLog("failed to read template from apk") - Logger.error("failed to read template from apk", it) - } - - output.write("</html>".toByteArray()) - output.close() - printLog("done") - } - - private fun exportJson(output: OutputStream) { - val rootObject = JsonObject().apply { - addProperty("conversationId", friendFeedInfo.key) - addProperty("conversationName", friendFeedInfo.feedDisplayName) - - var index = 0 - val participants = mutableMapOf<String, Int>() - - add("participants", JsonObject().apply { - conversationParticipants.forEach { (userId, friendInfo) -> - add(userId, JsonObject().apply { - addProperty("id", index) - addProperty("displayName", friendInfo.displayName) - addProperty("username", friendInfo.usernameForSorting) - addProperty("bitmojiSelfieId", friendInfo.bitmojiSelfieId) - }) - participants[userId] = index++ - } - }) - add("messages", JsonArray().apply { - messages.forEach { message -> - add(JsonObject().apply { - addProperty("orderKey", message.orderKey) - addProperty("senderId", participants.getOrDefault(message.senderId.toString(), -1)) - addProperty("type", message.messageContent.contentType.toString()) - - fun addUUIDList(name: String, list: List<SnapUUID>) { - add(name, JsonArray().apply { - list.map { participants.getOrDefault(it.toString(), -1) }.forEach { add(it) } - }) - } - - addUUIDList("savedBy", message.messageMetadata.savedBy) - addUUIDList("seenBy", message.messageMetadata.seenBy) - addUUIDList("openedBy", message.messageMetadata.openedBy) - - add("reactions", JsonObject().apply { - message.messageMetadata.reactions.forEach { reaction -> - addProperty( - participants.getOrDefault(reaction.userId.toString(), -1L).toString(), - reaction.reactionId - ) - } - }) - - addProperty("createdTimestamp", message.messageMetadata.createdAt) - addProperty("readTimestamp", message.messageMetadata.readAt) - addProperty("serializedContent", serializeMessageContent(message)) - addProperty("rawContent", Base64.getUrlEncoder().encodeToString(message.messageContent.content)) - - val messageContentType = message.messageContent.contentType - - EncryptionHelper.getEncryptionKeys(messageContentType, ProtoReader(message.messageContent.content), isArroyo = false)?.let { encryptionKeyPair -> - add("encryption", JsonObject().apply encryption@{ - addProperty("key", Base64.getEncoder().encodeToString(encryptionKeyPair.first)) - addProperty("iv", Base64.getEncoder().encodeToString(encryptionKeyPair.second)) - }) - } - - val remoteMediaReferences by lazy { - val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject - serializedMessageContent["mRemoteMediaReferences"] - .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } - .flatten() - } - - add("mediaReferences", JsonArray().apply mediaReferences@ { - if (messageContentType != ContentType.EXTERNAL_MEDIA && - messageContentType != ContentType.STICKER && - messageContentType != ContentType.SNAP && - messageContentType != ContentType.NOTE) - return@mediaReferences - - remoteMediaReferences.forEach { media -> - val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() - val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) - add(JsonObject().apply { - addProperty("mediaType", mediaType.toString()) - addProperty("content", Base64.getUrlEncoder().encodeToString(protoMediaReference)) - }) - } - }) - - }) - } - }) - } - - output.write(context.gson.toJson(rootObject).toByteArray()) - output.flush() - } - - suspend fun exportTo(exportFormat: ExportFormat) { - val output = FileOutputStream(outputFile) - - when (exportFormat) { - ExportFormat.HTML -> exportHtml(output) - ExportFormat.JSON -> exportJson(output) - ExportFormat.TEXT -> exportText(output) - } - - output.close() - } -}- \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts @@ -5,4 +5,7 @@ plugins { alias(libs.plugins.kotlinAndroid) apply false } +rootProject.ext.set("appVersionName", "1.1.0") +rootProject.ext.set("appVersionCode", 7) + true // Needed to make the Suppress annotation work for the plugins block \ No newline at end of file diff --git a/core/.gitignore b/core/.gitignore @@ -0,0 +1,16 @@ +*.iml +.gradle +/local.properties +/.idea/caches +/.idea/libraries +/.idea/modules.xml +/.idea/workspace.xml +/.idea/navEditor.xml +/.idea/assetWizardSettings.xml +/.idea/ +.DS_Store +/build +/captures +.externalNativeBuild +.cxx +local.properties diff --git a/core/build.gradle.kts b/core/build.gradle.kts @@ -0,0 +1,44 @@ +@Suppress("DSL_SCOPE_VIOLATION") // TODO: Remove once KTIJ-19369 is fixed +plugins { + id("com.android.library") + alias(libs.plugins.kotlinAndroid) +} +android { + namespace = "me.rhunk.snapenhance.core" + compileSdk = 33 + + buildFeatures { + aidl = true + } + + defaultConfig { + minSdk = 28 + buildConfigField("String", "VERSION_NAME", "\"${rootProject.ext["appVersionName"]}\"") + buildConfigField("int", "VERSION_CODE", "${rootProject.ext["appVersionCode"]}") + } + + kotlinOptions { + jvmTarget = "1.8" + } +} + +tasks.register("getVersion") { + doLast { + val versionFile = File("app/build/version.txt") + versionFile.writeText(android.defaultConfig.versionName.toString()) + } +} + +dependencies { + compileOnly(files("libs/LSPosed-api-1.0-SNAPSHOT.jar")) + implementation(libs.coroutines) + implementation(libs.kotlin.reflect) + implementation(libs.recyclerview) + implementation(libs.gson) + implementation(libs.ffmpeg.kit) + implementation(libs.osmdroid.android) + implementation(libs.okhttp) + implementation(libs.androidx.documentfile) + + implementation(project(":mapper")) +}+ \ No newline at end of file diff --git a/app/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar b/core/libs/LSPosed-api-1.0-SNAPSHOT-javadoc.jar Binary files differ. diff --git a/app/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar b/core/libs/LSPosed-api-1.0-SNAPSHOT-sources.jar Binary files differ. diff --git a/app/libs/LSPosed-api-1.0-SNAPSHOT.jar b/core/libs/LSPosed-api-1.0-SNAPSHOT.jar Binary files differ. diff --git a/app/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/BridgeInterface.aidl diff --git a/app/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl b/core/src/main/aidl/me/rhunk/snapenhance/bridge/DownloadCallback.aidl diff --git a/app/src/main/assets/lang/ar_SA.json b/core/src/main/assets/lang/ar_SA.json diff --git a/app/src/main/assets/lang/en_US.json b/core/src/main/assets/lang/en_US.json diff --git a/app/src/main/assets/lang/fr_FR.json b/core/src/main/assets/lang/fr_FR.json diff --git a/app/src/main/assets/lang/hi_IN.json b/core/src/main/assets/lang/hi_IN.json diff --git a/app/src/main/assets/web/export_template.html b/core/src/main/assets/web/export_template.html diff --git a/app/src/main/assets/web/rawinflate.js b/core/src/main/assets/web/rawinflate.js diff --git a/app/src/main/assets/xposed_init b/core/src/main/assets/xposed_init diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/Constants.kt b/core/src/main/kotlin/me/rhunk/snapenhance/Constants.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/Logger.kt @@ -0,0 +1,43 @@ +package me.rhunk.snapenhance + +import android.util.Log +import de.robv.android.xposed.XposedBridge +import me.rhunk.snapenhance.core.BuildConfig + +object Logger { + private const val TAG = "SnapEnhance" + + fun log(message: Any?) { + Log.i(TAG, message.toString()) + } + + fun debug(message: Any?) { + if (!BuildConfig.DEBUG) return + Log.d(TAG, message.toString()) + } + + fun error(throwable: Throwable) { + Log.e(TAG, "", throwable) + } + + fun error(message: Any?) { + Log.e(TAG, message.toString()) + } + + fun error(message: Any?, throwable: Throwable) { + Log.e(TAG, message.toString(), throwable) + } + + fun xposedLog(message: Any?) { + XposedBridge.log(message.toString()) + } + + fun xposedLog(message: Any?, throwable: Throwable?) { + XposedBridge.log(message.toString()) + XposedBridge.log(throwable) + } + + fun xposedLog(throwable: Throwable) { + XposedBridge.log(throwable) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ModContext.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SharedContext.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt b/core/src/main/kotlin/me/rhunk/snapenhance/SnapEnhance.kt @@ -0,0 +1,122 @@ +package me.rhunk.snapenhance + +import android.app.Activity +import android.app.Application +import android.content.Context +import android.content.pm.PackageManager +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.bridge.BridgeClient +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.data.SnapClassCache +import me.rhunk.snapenhance.hook.HookStage +import me.rhunk.snapenhance.hook.Hooker +import me.rhunk.snapenhance.hook.hook +import me.rhunk.snapenhance.util.getApplicationInfoCompat +import kotlin.time.ExperimentalTime +import kotlin.time.measureTime + +class SnapEnhance { + companion object { + lateinit var classLoader: ClassLoader + val classCache: SnapClassCache by lazy { + SnapClassCache(classLoader) + } + } + private val appContext = ModContext() + private var isBridgeInitialized = false + + init { + Hooker.hook(Application::class.java, "attach", HookStage.BEFORE) { param -> + appContext.androidContext = param.arg<Context>(0).also { + classLoader = it.classLoader + } + appContext.bridgeClient = BridgeClient(appContext) + + //for lspatch builds, we need to check if the service is correctly installed + runCatching { + appContext.androidContext.packageManager.getApplicationInfoCompat(BuildConfig.LIBRARY_PACKAGE_NAME, PackageManager.GET_META_DATA) + }.onFailure { + appContext.crash("SnapEnhance bridge service is not installed. Please download stable version from https://github.com/rhunk/SnapEnhance/releases") + return@hook + } + + appContext.bridgeClient.apply { + start { bridgeResult -> + if (!bridgeResult) { + Logger.xposedLog("Cannot connect to bridge service") + appContext.softRestartApp() + return@start + } + runCatching { + runBlocking { + init() + } + }.onSuccess { + isBridgeInitialized = true + }.onFailure { + Logger.xposedLog("Failed to initialize", it) + } + } + } + } + + Activity::class.java.hook( "onCreate", HookStage.AFTER, { isBridgeInitialized }) { + val activity = it.thisObject() as Activity + if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook + val isMainActivityNotNull = appContext.mainActivity != null + appContext.mainActivity = activity + if (isMainActivityNotNull || !appContext.mappings.areMappingsLoaded) return@hook + onActivityCreate() + } + + var activityWasResumed = false + + //we need to reload the config when the app is resumed + Activity::class.java.hook("onResume", HookStage.AFTER, { isBridgeInitialized }) { + val activity = it.thisObject() as Activity + + if (!activity.packageName.equals(Constants.SNAPCHAT_PACKAGE_NAME)) return@hook + + if (!activityWasResumed) { + activityWasResumed = true + return@hook + } + + Logger.debug("Reloading config") + appContext.config.loadFromBridge(appContext.bridgeClient) + } + } + + @OptIn(ExperimentalTime::class) + private suspend fun init() { + //load translations in a coroutine to speed up initialization + withContext(appContext.coroutineDispatcher) { + appContext.translation.loadFromBridge(appContext.bridgeClient) + } + + measureTime { + with(appContext) { + config.loadFromBridge(bridgeClient) + mappings.init() + //if mappings aren't loaded, we can't initialize features + if (!mappings.areMappingsLoaded) return + features.init() + } + }.also { time -> + Logger.debug("init took $time") + } + } + + @OptIn(ExperimentalTime::class) + private fun onActivityCreate() { + measureTime { + with(appContext) { + features.onActivityCreate() + actionManager.init() + } + }.also { time -> + Logger.debug("onActivityCreate took $time") + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/XposedLoader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/XposedLoader.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/AbstractAction.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CheckForUpdates.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/CleanCache.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ClearMessageLogger.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/ExportChatMessages.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/OpenMap.kt @@ -0,0 +1,23 @@ +package me.rhunk.snapenhance.action.impl + +import android.content.Intent +import android.os.Bundle +import me.rhunk.snapenhance.action.AbstractAction +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.ui.map.MapActivity + +class OpenMap: AbstractAction("action.open_map", dependsOnProperty = ConfigProperty.LOCATION_SPOOF) { + override fun run() { + context.runOnUiThread { + val mapActivityIntent = Intent() + mapActivityIntent.setClassName(BuildConfig.LIBRARY_PACKAGE_NAME, MapActivity::class.java.name) + mapActivityIntent.putExtra("location", Bundle().apply { + putDouble("latitude", context.config.string(ConfigProperty.LATITUDE).toDouble()) + putDouble("longitude", context.config.string(ConfigProperty.LONGITUDE).toDouble()) + }) + + context.mainActivity!!.startActivityForResult(mapActivityIntent, 0x1337) + } + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt b/core/src/main/kotlin/me/rhunk/snapenhance/action/impl/RefreshMappings.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeClient.kt @@ -0,0 +1,126 @@ +package me.rhunk.snapenhance.bridge + + +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.ServiceConnection +import android.os.Build +import android.os.Handler +import android.os.HandlerThread +import android.os.IBinder +import de.robv.android.xposed.XposedHelpers +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.Logger.xposedLog +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.data.LocalePair +import java.util.concurrent.CompletableFuture +import java.util.concurrent.Executors +import kotlin.system.exitProcess + + +class BridgeClient( + private val context: ModContext +): ServiceConnection { + private lateinit var future: CompletableFuture<Boolean> + private lateinit var service: BridgeInterface + + fun start(callback: (Boolean) -> Unit) { + this.future = CompletableFuture() + + with(context.androidContext) { + //ensure the remote process is running + startActivity(Intent() + .setClassName(BuildConfig.LIBRARY_PACKAGE_NAME, ForceStartActivity::class.java.name) + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) + ) + + val intent = Intent() + .setClassName(BuildConfig.LIBRARY_PACKAGE_NAME, BridgeService::class.java.name) + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) { + bindService( + intent, + Context.BIND_AUTO_CREATE, + Executors.newSingleThreadExecutor(), + this@BridgeClient + ) + } else { + XposedHelpers.callMethod( + this, + "bindServiceAsUser", + intent, + this@BridgeClient, + Context.BIND_AUTO_CREATE, + Handler(HandlerThread("BridgeClient").apply { + start() + }.looper), + android.os.Process.myUserHandle() + ) + } + } + callback(future.get()) + } + + + override fun onServiceConnected(name: ComponentName, service: IBinder) { + this.service = BridgeInterface.Stub.asInterface(service) + future.complete(true) + } + + override fun onNullBinding(name: ComponentName) { + xposedLog("failed to connect to bridge service") + future.complete(false) + } + + override fun onServiceDisconnected(name: ComponentName) { + exitProcess(0) + } + + fun createAndReadFile( + fileType: BridgeFileType, + defaultContent: ByteArray + ): ByteArray = service.createAndReadFile(fileType.value, defaultContent) + + fun readFile(fileType: BridgeFileType): ByteArray = service.readFile(fileType.value) + + fun writeFile( + fileType: BridgeFileType, + content: ByteArray? + ): Boolean = service.writeFile(fileType.value, content) + + fun deleteFile(fileType: BridgeFileType) = service.deleteFile(fileType.value) + + + fun isFileExists(fileType: BridgeFileType) = service.isFileExists(fileType.value) + + fun getLoggedMessageIds(conversationId: String, limit: Int): LongArray = service.getLoggedMessageIds(conversationId, limit) + + fun getMessageLoggerMessage(conversationId: String, id: Long): ByteArray? = service.getMessageLoggerMessage(conversationId, id) + + fun addMessageLoggerMessage(conversationId: String,id: Long, message: ByteArray) = service.addMessageLoggerMessage(conversationId, id, message) + + fun deleteMessageLoggerMessage(conversationId: String, id: Long) = service.deleteMessageLoggerMessage(conversationId, id) + + fun clearMessageLogger() = service.clearMessageLogger() + + fun fetchTranslations() = service.fetchTranslations().map { + LocalePair(it.key, it.value) + } + + fun getAutoUpdaterTime(): Long { + createAndReadFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, "0".toByteArray()).run { + return if (isEmpty()) { + 0 + } else { + String(this).toLong() + } + } + } + + fun setAutoUpdaterTime(time: Long) { + writeFile(BridgeFileType.AUTO_UPDATER_TIMESTAMP, time.toString().toByteArray()) + } + + fun enqueueDownload(intent: Intent, callback: DownloadCallback) = service.enqueueDownload(intent, callback) +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/BridgeService.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/ForceStartActivity.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/types/BridgeFileType.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/ConfigWrapper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/MessageLoggerWrapper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/bridge/wrapper/TranslationWrapper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigAccessor.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigCategory.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigProperty.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/ConfigValue.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigIntegerValue.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateListValue.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateSelection.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStateValue.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt b/core/src/main/kotlin/me/rhunk/snapenhance/config/impl/ConfigStringValue.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/FileType.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/LocalePair.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/LocalePair.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/MessageSender.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/SnapClassCache.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/SnapEnums.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/AbstractWrapper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/FriendActionButton.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/FriendActionButton.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/Message.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageContent.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDescriptor.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageDestinations.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/MessageMetadata.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/ScSize.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/ScSize.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/SnapUUID.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/UserIdToReaction.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/EncryptionWrapper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/MediaInfo.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/LongformVideoPlaylistItem.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapChapter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapChapter.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapPlaylistItem.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/dash/SnapPlaylistItem.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/Layer.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/LayerController.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/data/wrapper/impl/media/opera/ParamMap.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseAccess.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/DatabaseObject.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/ConversationMessage.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendFeedInfo.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/FriendInfo.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/StoryEntry.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt b/core/src/main/kotlin/me/rhunk/snapenhance/database/objects/UserConversationLink.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadManagerClient.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadProcessor.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/DownloadTaskManager.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadMetadata.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/DownloadRequest.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/MediaEncryptionKeyPair.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/PendingDownload.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/data/SplitMediaAssetType.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadMediaType.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadMediaType.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadStage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/download/enums/DownloadStage.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/event/EventBus.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt b/core/src/main/kotlin/me/rhunk/snapenhance/event/Events.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/BridgeFileFeature.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/Feature.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/FeatureLoadParams.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/AutoUpdater.kt @@ -0,0 +1,110 @@ +package me.rhunk.snapenhance.features.impl + +import android.annotation.SuppressLint +import android.app.AlertDialog +import android.app.DownloadManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import android.net.Uri +import android.os.Environment +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.features.Feature +import me.rhunk.snapenhance.features.FeatureLoadParams +import me.rhunk.snapenhance.ui.ViewAppearanceHelper +import okhttp3.OkHttpClient +import okhttp3.Request +import org.json.JSONArray + +class AutoUpdater : Feature("AutoUpdater", loadParams = FeatureLoadParams.ACTIVITY_CREATE_ASYNC) { + override fun asyncOnActivityCreate() { + val checkForUpdateMode = context.config.state(ConfigProperty.AUTO_UPDATER) + val currentTimeMillis = System.currentTimeMillis() + val checkForUpdatesTimestamp = context.bridgeClient.getAutoUpdaterTime() + + val delayTimestamp = when (checkForUpdateMode) { + "EVERY_LAUNCH" -> currentTimeMillis - checkForUpdatesTimestamp + "DAILY" -> 86400000L + "WEEKLY" -> 604800000L + else -> return + } + + if (checkForUpdatesTimestamp + delayTimestamp > currentTimeMillis) return + + runCatching { + checkForUpdates() + }.onFailure { + Logger.error("Failed to check for updates: ${it.message}", it) + }.onSuccess { + context.bridgeClient.setAutoUpdaterTime(currentTimeMillis) + } + } + + @SuppressLint("UnspecifiedRegisterReceiverFlag") + fun checkForUpdates(): String? { + val endpoint = Request.Builder().url("https://api.github.com/repos/rhunk/SnapEnhance/releases").build() + val response = OkHttpClient().newCall(endpoint).execute() + + if (!response.isSuccessful) throw Throwable("Failed to fetch releases: ${response.code}") + + val releases = JSONArray(response.body.string()).also { + if (it.length() == 0) throw Throwable("No releases found") + } + + val latestRelease = releases.getJSONObject(0) + val latestVersion = latestRelease.getString("tag_name") + if (latestVersion.removePrefix("v") == BuildConfig.VERSION_NAME) return null + + val releaseContentBody = latestRelease.getString("body") + val downloadEndpoint = latestRelease.getJSONArray("assets").getJSONObject(0).getString("browser_download_url") + + context.runOnUiThread { + ViewAppearanceHelper.newAlertDialogBuilder(context.mainActivity) + .setTitle(context.translation["auto_updater.dialog_title"]) + .setMessage( + context.translation.format("auto_updater.dialog_message", + "version" to latestVersion, + "body" to releaseContentBody) + ) + .setNegativeButton(context.translation["auto_updater.dialog_negative_button"]) { dialog, _ -> + dialog.dismiss() + } + .setPositiveButton(context.translation["auto_updater.dialog_positive_button"]) { dialog, _ -> + dialog.dismiss() + context.longToast(context.translation["auto_updater.downloading_toast"]) + + val request = DownloadManager.Request(Uri.parse(downloadEndpoint)) + .setTitle(context.translation["auto_updater.download_manager_notification_title"]) + .setDestinationInExternalPublicDir(Environment.DIRECTORY_DOWNLOADS, "latest-snapenhance.apk") + .setMimeType("application/vnd.android.package-archive") + .setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + + val downloadManager = context.androidContext.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + val downloadId = downloadManager.enqueue(request) + + val onCompleteReceiver = object: BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) + if (id != downloadId) return + context.unregisterReceiver(this) + context.startActivity( + Intent(Intent.ACTION_VIEW).apply { + setDataAndType(downloadManager.getUriForDownloadedFile(downloadId), "application/vnd.android.package-archive") + flags = Intent.FLAG_ACTIVITY_NEW_TASK + } + ) + } + } + + context.mainActivity?.registerReceiver(onCompleteReceiver, IntentFilter( + DownloadManager.ACTION_DOWNLOAD_COMPLETE + )) + }.show() + } + + return latestVersion + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ConfigEnumKeys.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/Messaging.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/AntiAutoDownload.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/downloader/MediaDownloader.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AmoledDarkMode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AmoledDarkMode.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/AppPasscode.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/DeviceSpooferHook.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/InfiniteStoryBoost.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/InfiniteStoryBoost.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/MeoPasscodeBypass.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/experiments/UnlimitedMultiSnap.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/DisableMetrics.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/privacy/PreventMessageSending.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/AnonymousStoryViewing.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/MessageLogger.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/PreventReadReceipts.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/spying/StealthMode.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AntiAutoSave.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/AutoSave.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/CameraTweaks.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/DisableVideoLengthRestriction.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GalleryMediaSendOverride.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/GooglePlayServicesDialogs.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/LocationSpoofer.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/MediaQualityLevelOverride.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/Notifications.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/SnapchatPlus.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/tweaks/UnlimitedSnapViewTime.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/PinConversations.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/StartupPageOverride.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt b/core/src/main/kotlin/me/rhunk/snapenhance/features/impl/ui/UITweaks.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/HookAdapter.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/HookStage.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt b/core/src/main/kotlin/me/rhunk/snapenhance/hook/Hooker.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/Manager.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ActionManager.kt @@ -0,0 +1,41 @@ +package me.rhunk.snapenhance.manager.impl + +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.action.AbstractAction +import me.rhunk.snapenhance.action.impl.CheckForUpdates +import me.rhunk.snapenhance.action.impl.CleanCache +import me.rhunk.snapenhance.action.impl.ClearMessageLogger +import me.rhunk.snapenhance.action.impl.ExportChatMessages +import me.rhunk.snapenhance.action.impl.OpenMap +import me.rhunk.snapenhance.action.impl.RefreshMappings +import me.rhunk.snapenhance.manager.Manager +import kotlin.reflect.KClass + +class ActionManager( + private val context: ModContext, +) : Manager { + private val actions = mutableMapOf<String, AbstractAction>() + fun getActions() = actions.values.toList() + private fun load(clazz: KClass<out AbstractAction>) { + val action = clazz.java.newInstance() + action.context = context + actions[action.nameKey] = action + } + override fun init() { + load(CleanCache::class) + load(ExportChatMessages::class) + load(OpenMap::class) + + if(!BuildConfig.DEBUG) { + load(CheckForUpdates::class) + } + else { + load(ClearMessageLogger::class) + load(RefreshMappings::class) + } + + + actions.values.forEach(AbstractAction::init) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/ConfigManager.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/FeatureManager.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt b/core/src/main/kotlin/me/rhunk/snapenhance/manager/impl/MappingManager.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/ItemHelper.kt @@ -0,0 +1,85 @@ +package me.rhunk.snapenhance.ui + +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.widget.EditText +import android.widget.TextView +import android.widget.Toast +import me.rhunk.snapenhance.core.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.util.ActivityResultCallback +import kotlin.math.abs +import kotlin.random.Random + +class ItemHelper( + private val context : Context +) { + val positiveButtonText by lazy { + SharedContext.translation["button.ok"] + } + + val cancelButtonText by lazy { + SharedContext.translation["button.cancel"] + } + + fun longToast(message: String, context: Context) { + Toast.makeText(context, message, Toast.LENGTH_LONG).show() + } + + fun createTranslatedTextView(property: ConfigProperty, shouldTranslatePropertyValue: Boolean = true): TextView { + return object: TextView(context) { + override fun setText(text: CharSequence?, type: BufferType?) { + val newText = text?.takeIf { it.isNotEmpty() }?.let { + if (!shouldTranslatePropertyValue || property.disableValueLocalization) it + else SharedContext.translation[property.getOptionTranslationKey(it.toString())] + }?.let { + if (it.length > 20) { + "...${it.substring(it.length - 20)}" + } else { + it + } + } ?: "" + super.setTextColor(context.getColor(R.color.tertiaryText)) + super.setText(newText, type) + } + } + } + + fun askForValue(property: ConfigProperty, requestedInputType: Int, callback: (String) -> Unit) { + val editText = EditText(context).apply { + inputType = requestedInputType + setText(property.valueContainer.value().toString()) + } + AlertDialog.Builder(context) + .setTitle(SharedContext.translation["property.${property.translationKey}.name"]) + .setView(editText) + .setPositiveButton(positiveButtonText) { _, _ -> + callback(editText.text.toString()) + } + .setNegativeButton(cancelButtonText) { dialog, _ -> + dialog.cancel() + } + .show() + } + + fun askForFolder(activity: Activity, property: ConfigProperty, callback: (String) -> Unit): Pair<Int, ActivityResultCallback> { + val intent = Intent(Intent.ACTION_OPEN_DOCUMENT_TREE) + .addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + .addFlags(Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + + val requestCode = abs(Random.nextInt()) + activity.startActivityForResult(intent, requestCode) + + return requestCode to let@{_, resultCode, data -> + if (resultCode != Activity.RESULT_OK) return@let + val uri = data?.data ?: return@let + val value = uri.toString() + activity.contentResolver.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_GRANT_WRITE_URI_PERMISSION) + property.valueContainer.writeFrom(value) + callback(value) + } + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/ViewAppearanceHelper.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/config/ConfigActivity.kt @@ -0,0 +1,279 @@ +package me.rhunk.snapenhance.ui.config + +import android.app.Activity +import android.app.AlertDialog +import android.content.Intent +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.Html +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Switch +import android.widget.TextView +import android.widget.Toast +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.config.ConfigCategory +import me.rhunk.snapenhance.config.ConfigProperty +import me.rhunk.snapenhance.config.impl.ConfigIntegerValue +import me.rhunk.snapenhance.config.impl.ConfigStateListValue +import me.rhunk.snapenhance.config.impl.ConfigStateSelection +import me.rhunk.snapenhance.config.impl.ConfigStateValue +import me.rhunk.snapenhance.config.impl.ConfigStringValue +import me.rhunk.snapenhance.ui.ItemHelper +import me.rhunk.snapenhance.util.ActivityResultCallback +import kotlin.system.exitProcess + + +class ConfigActivity : Activity() { + private val itemHelper = ItemHelper(this) + private val activityResultCallbacks = mutableMapOf<Int, ActivityResultCallback>() + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onBackPressed() { + super.onBackPressed() + finish() + } + + override fun onDestroy() { + super.onDestroy() + SharedContext.config.writeConfig() + } + + override fun onPause() { + super.onPause() + SharedContext.config.writeConfig() + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + activityResultCallbacks[requestCode]?.invoke(requestCode, resultCode, data) + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + SharedContext.ensureInitialized(this) + setContentView(R.layout.config_activity) + + findViewById<View>(R.id.title_bar).let { titleBar -> + titleBar.findViewById<TextView>(R.id.title).text = SharedContext.translation["config_activity.title"] + titleBar.findViewById<ImageButton>(R.id.back_button).visibility = View.GONE + } + + val propertyListLayout = findViewById<ViewGroup>(R.id.property_list) + + if (intent.getBooleanExtra("lspatched", false) || + applicationInfo.packageName != "me.rhunk.snapenhance" || + BuildConfig.DEBUG) { + propertyListLayout.addView( + layoutInflater.inflate( + R.layout.config_activity_debug_item, + propertyListLayout, + false + ).apply { + findViewById<TextView>(R.id.debug_item_content).apply { + text = Html.fromHtml( + "You are using a <u><b>debug/unofficial</b></u> build!\n" + + "Please consider downloading stable builds from <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>.", + Html.FROM_HTML_MODE_COMPACT + ) + movementMethod = android.text.method.LinkMovementMethod.getInstance() + } + }) + } + + //check if save folder is set + //TODO: first run activity + run { + val saveFolder = SharedContext.config.string(ConfigProperty.SAVE_FOLDER) + val itemHelper = ItemHelper(this) + + if (saveFolder.isEmpty() || !saveFolder.startsWith("content://")) { + AlertDialog.Builder(this) + .setTitle("Save folder") + .setMessage("Please select a folder where you want to save downloaded files.") + .setPositiveButton("Select") { _, _ -> + val (requestCode, callback) = itemHelper.askForFolder( + this, + ConfigProperty.SAVE_FOLDER + ) {} + activityResultCallbacks[requestCode] = { a1, a2, a3 -> + callback(a1, a2, a3) + Toast.makeText(this, "Save Folder set!", Toast.LENGTH_SHORT).show() + finish() + } + } + .setNegativeButton("Cancel") { _, _ -> + exitProcess(0) + } + .show() + } + } + + var currentCategory: ConfigCategory? = null + + SharedContext.config.entries().filter { !it.key.category.hidden }.forEach { (property, value) -> + val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) + + fun addSeparator() { + //add separator + propertyListLayout.addView(View(this).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) + setBackgroundColor(getColor(R.color.tertiaryBackground)) + }) + } + + if (property.category != currentCategory) { + if(!property.shouldAppearInSettings) return@forEach + currentCategory = property.category + with(layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false)) { + findViewById<TextView>(R.id.name).apply { + text = SharedContext.translation["category.${property.category.key}"] + textSize = 20f + typeface = typeface?.let { android.graphics.Typeface.create(it, android.graphics.Typeface.BOLD) } + } + propertyListLayout.addView(this) + } + addSeparator() + } + + if (!property.shouldAppearInSettings) return@forEach + + val propertyName = SharedContext.translation["property.${property.translationKey}.name"] + + configItem.findViewById<TextView>(R.id.name).text = propertyName + configItem.findViewById<TextView>(R.id.description).also { + it.text = SharedContext.translation["property.${property.translationKey}.description"] + it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE + } + + fun addValueView(view: View) { + configItem.findViewById<ViewGroup>(R.id.value).addView(view) + } + + when (value) { + is ConfigStateValue -> { + val switch = Switch(this) + switch.isChecked = value.value() + switch.trackTintList = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + switch.highlightColor, + getColor(R.color.tertiaryBackground) + ) + ) + switch.setOnCheckedChangeListener { _, isChecked -> + value.writeFrom(isChecked.toString()) + } + configItem.setOnClickListener { switch.toggle() } + addValueView(switch) + } + is ConfigStringValue, is ConfigIntegerValue -> { + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { + it.text = value.value().toString() + } + configItem.setOnClickListener { + if (value is ConfigStringValue && value.isFolderPath) { + val (requestCode, callback) = itemHelper.askForFolder(this, property) { + value.writeFrom(it) + textView.text = value.value() + } + + activityResultCallbacks[requestCode] = callback + return@setOnClickListener + } + + if (value is ConfigIntegerValue) { + itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) { + try { + value.writeFrom(it) + textView.text = value.value().toString() + } catch (e: NumberFormatException) { + itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this) + } + } + return@setOnClickListener + } + itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) { + value.writeFrom(it) + textView.text = value.value().toString() + } + } + addValueView(textView) + } + is ConfigStateListValue -> { + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false) + val values = value.value() + + fun updateText() { + textView.text = SharedContext.translation.format("config_activity.selected_text", "count" to values.filter { it.value }.size.toString()) + } + + updateText() + + configItem.setOnClickListener { + AlertDialog.Builder(this) + .setTitle(propertyName) + .setPositiveButton(itemHelper.positiveButtonText) { _, _ -> + updateText() + } + .setMultiChoiceItems( + values.keys.map { + if (property.disableValueLocalization) it + else SharedContext.translation[property.getOptionTranslationKey(it)] + }.toTypedArray(), + values.map { it.value }.toBooleanArray() + ) { _, which, isChecked -> + value.setKey(values.keys.elementAt(which), isChecked) + } + .show() + } + + addValueView(textView) + } + is ConfigStateSelection -> { + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = true) + textView.text = value.value() + + configItem.setOnClickListener { + val builder = AlertDialog.Builder(this) + builder.setTitle(propertyName) + + builder.setSingleChoiceItems( + value.keys().toTypedArray().map { + if (property.disableValueLocalization) it + else SharedContext.translation[property.getOptionTranslationKey(it)] + }.toTypedArray(), + value.keys().indexOf(value.value()) + ) { _, which -> + value.writeFrom(value.keys()[which]) + } + + builder.setPositiveButton(itemHelper.positiveButtonText) { _, _ -> + textView.text = value.value() + } + + builder.show() + } + addValueView(textView) + } + } + + propertyListLayout.addView(configItem) + addSeparator() + } + + propertyListLayout.addView(layoutInflater.inflate(R.layout.config_activity_debug_item, propertyListLayout, false).apply { + findViewById<TextView>(R.id.debug_item_content).apply { + text = Html.fromHtml("Made by rhunk on <a href=\"https://github.com/rhunk/SnapEnhance\">GitHub</a>", Html.FROM_HTML_MODE_COMPACT) + movementMethod = android.text.method.LinkMovementMethod.getInstance() + } + }) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DebugSettingsLayoutInflater.kt @@ -0,0 +1,113 @@ +package me.rhunk.snapenhance.ui.download + +import android.app.AlertDialog +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import android.widget.ImageButton +import android.widget.ListView +import android.widget.TextView +import android.widget.Toast +import me.rhunk.snapenhance.core.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.bridge.types.BridgeFileType +import me.rhunk.snapenhance.ui.config.ConfigActivity +import me.rhunk.snapenhance.ui.spoof.DeviceSpooferActivity +import java.io.File + +class ActionListAdapter( + private val activity: DownloadManagerActivity, + private val layoutId: Int, + private val actions: Array<Pair<String, () -> Unit>> +) : ArrayAdapter<Pair<String, () -> Unit>>(activity, layoutId, actions) { + override fun getView(position: Int, convertView: View?, parent: ViewGroup): View { + val view = convertView ?: activity.layoutInflater.inflate(layoutId, parent, false) + val action = actions[position] + view.isClickable = true + + view.findViewById<TextView>(R.id.feature_text).text = action.first + view.setOnClickListener { + action.second() + } + + return view + } +} + +class DebugSettingsLayoutInflater( + private val activity: DownloadManagerActivity +) { + private fun confirmAction(title: String, message: String, action: () -> Unit) { + activity.runOnUiThread { + AlertDialog.Builder(activity) + .setTitle(title) + .setMessage(message) + .setPositiveButton(SharedContext.translation["button.positive"]) { _, _ -> + action() + } + .setNegativeButton(SharedContext.translation["button.negative"]) { _, _ -> } + .show() + } + } + + private fun showSuccessToast() { + Toast.makeText(activity, "Success", Toast.LENGTH_SHORT).show() + } + + fun inflate(parent: ViewGroup) { + val debugSettingsLayout = activity.layoutInflater.inflate(R.layout.debug_settings_page, parent, false) + + val debugSettingsTranslation = activity.translation.getCategory("debug_settings_page") + + debugSettingsLayout.findViewById<ImageButton>(R.id.back_button).setOnClickListener { + parent.removeView(debugSettingsLayout) + } + + debugSettingsLayout.findViewById<TextView>(R.id.title).text = activity.translation["debug_settings"] + + debugSettingsLayout.findViewById<ListView>(R.id.setting_page_list).apply { + adapter = ActionListAdapter(activity, R.layout.debug_setting_item, mutableListOf<Pair<String, () -> Unit>>().apply { + add(SharedContext.translation["config_activity.title"] to { + activity.startActivity(Intent(activity, ConfigActivity::class.java)) + }) + add(SharedContext.translation["spoof_activity.title"] to { + activity.startActivity(Intent(activity, DeviceSpooferActivity::class.java)) + }) + add(debugSettingsTranslation["clear_cache_title"] to { + context.cacheDir.listFiles()?.forEach { + it.deleteRecursively() + } + showSuccessToast() + }) + + BridgeFileType.values().forEach { fileType -> + val actionName = debugSettingsTranslation.format("clear_file_title", "file_name" to fileType.displayName) + add(actionName to { + confirmAction(actionName, debugSettingsTranslation.format("clear_file_confirmation", "file_name" to fileType.displayName)) { + fileType.resolve(context).deleteRecursively() + showSuccessToast() + } + }) + } + + add(debugSettingsTranslation["reset_all_title"] to { + confirmAction(debugSettingsTranslation["reset_all_title"], debugSettingsTranslation["reset_all_confirmation"]) { + arrayOf(context.cacheDir, context.filesDir, File(context.dataDir, "databases"), File(context.dataDir, "shared_prefs")).forEach { + it.listFiles()?.forEach { file -> + file.deleteRecursively() + } + } + showSuccessToast() + } + }) + }.toTypedArray()) + } + + activity.registerBackCallback { + parent.removeView(debugSettingsLayout) + } + + parent.addView(debugSettingsLayout) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadListAdapter.kt @@ -0,0 +1,242 @@ +package me.rhunk.snapenhance.ui.download + +import android.annotation.SuppressLint +import android.content.Intent +import android.graphics.Bitmap +import android.graphics.BitmapFactory +import android.net.Uri +import android.os.Handler +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.Button +import android.widget.ImageView +import android.widget.TextView +import android.widget.Toast +import androidx.core.graphics.drawable.RoundedBitmapDrawableFactory +import androidx.recyclerview.widget.RecyclerView +import androidx.recyclerview.widget.RecyclerView.Adapter +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.job +import kotlinx.coroutines.launch +import kotlinx.coroutines.withTimeout +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.core.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.download.data.PendingDownload +import me.rhunk.snapenhance.download.enums.DownloadStage +import me.rhunk.snapenhance.util.snap.PreviewUtils +import java.io.File +import java.io.FileInputStream +import java.net.URL +import kotlin.concurrent.thread +import kotlin.coroutines.coroutineContext + +class DownloadListAdapter( + private val activity: DownloadManagerActivity, + private val downloadList: MutableList<PendingDownload> +): Adapter<DownloadListAdapter.ViewHolder>() { + private val coroutineScope = CoroutineScope(Dispatchers.IO) + private val previewJobs = mutableMapOf<Int, Job>() + + inner class ViewHolder(val view: View) : RecyclerView.ViewHolder(view) { + val bitmojiIcon: ImageView = view.findViewById(R.id.bitmoji_icon) + val title: TextView = view.findViewById(R.id.item_title) + val subtitle: TextView = view.findViewById(R.id.item_subtitle) + val status: TextView = view.findViewById(R.id.item_status) + val actionButton: Button = view.findViewById(R.id.item_action_button) + val radius by lazy { + view.context.resources.getDimensionPixelSize(R.dimen.download_manager_item_preview_radius) + } + val viewWidth by lazy { + view.resources.displayMetrics.widthPixels + } + val viewHeight by lazy { + view.layoutParams.height + } + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ViewHolder { + return ViewHolder(LayoutInflater.from(parent.context).inflate(R.layout.download_manager_item, parent, false)) + } + + override fun getItemCount(): Int { + return downloadList.size + } + + @SuppressLint("Recycle") + private suspend fun handlePreview(download: PendingDownload, holder: ViewHolder) { + download.outputFile?.let { + val uri = Uri.parse(it) + runCatching { + if (uri.scheme == "content") { + val fileType = activity.contentResolver.openInputStream(uri)!!.use { stream -> + FileType.fromInputStream(stream) + } + fileType to activity.contentResolver.openInputStream(uri) + } else { + FileType.fromFile(File(it)) to FileInputStream(it) + } + }.getOrNull() + }?.also { (fileType, assetStream) -> + val previewBitmap = assetStream?.use { stream -> + //don't preview files larger than 30MB + if (stream.available() > 30 * 1024 * 1024) return@also + + val tempFile = File.createTempFile("preview", ".${fileType.fileExtension}") + tempFile.outputStream().use { output -> + stream.copyTo(output) + } + runCatching { + PreviewUtils.createPreviewFromFile(tempFile)?.let { preview -> + val offsetY = (preview.height / 2 - holder.viewHeight / 2).coerceAtLeast(0) + + Bitmap.createScaledBitmap( + Bitmap.createBitmap( + preview, 0, offsetY, + preview.width.coerceAtMost(holder.viewWidth), + preview.height.coerceAtMost(holder.viewHeight) + ), + holder.viewWidth, + holder.viewHeight, + false + ) + } + }.onFailure { + Logger.error("failed to create preview $fileType", it) + }.also { + tempFile.delete() + }.getOrNull() + } ?: return@also + + if (coroutineContext.job.isCancelled) return@also + Handler(holder.view.context.mainLooper).post { + holder.view.background = RoundedBitmapDrawableFactory.create( + holder.view.context.resources, + previewBitmap + ).also { + it.cornerRadius = holder.radius.toFloat() + } + } + } + } + + private fun updateViewHolder(download: PendingDownload, holder: ViewHolder) { + holder.status.text = download.downloadStage.toString() + holder.view.background = holder.view.context.getDrawable(R.drawable.download_manager_item_background) + + coroutineScope.launch { + withTimeout(2000) { + handlePreview(download, holder) + } + } + + val isSaved = download.downloadStage == DownloadStage.SAVED + //if the download is in progress, the user can cancel it + val canInteract = if (download.job != null) !download.downloadStage.isFinalStage || isSaved + else isSaved + + holder.status.visibility = if (isSaved) View.GONE else View.VISIBLE + + with(holder.actionButton) { + isEnabled = canInteract + alpha = if (canInteract) 1f else 0.5f + background = context.getDrawable(if (isSaved) R.drawable.action_button_success else R.drawable.action_button_cancel) + setTextColor(context.getColor(if (isSaved) R.color.successColor else R.color.actionBarColor)) + text = if (isSaved) + SharedContext.translation["button.open"] + else + SharedContext.translation["button.cancel"] + } + } + + override fun onBindViewHolder(holder: ViewHolder, position: Int) { + val pendingDownload = downloadList[position] + + pendingDownload.changeListener = { _, _ -> + Handler(holder.view.context.mainLooper).post { + updateViewHolder(pendingDownload, holder) + notifyItemChanged(position) + } + } + + holder.bitmojiIcon.setImageResource(R.drawable.bitmoji_blank) + + pendingDownload.metadata.iconUrl?.let { url -> + thread(start = true) { + runCatching { + val iconBitmap = URL(url).openStream().use { + BitmapFactory.decodeStream(it) + } + Handler(holder.view.context.mainLooper).post { + holder.bitmojiIcon.setImageBitmap(iconBitmap) + } + } + } + } + + holder.title.visibility = View.GONE + holder.subtitle.visibility = View.GONE + + pendingDownload.metadata.mediaDisplayType?.let { + holder.title.text = it + holder.title.visibility = View.VISIBLE + } + + pendingDownload.metadata.mediaDisplaySource?.let { + holder.subtitle.text = it + holder.subtitle.visibility = View.VISIBLE + } + + holder.actionButton.setOnClickListener { + if (pendingDownload.downloadStage != DownloadStage.SAVED) { + pendingDownload.cancel() + pendingDownload.downloadStage = DownloadStage.CANCELLED + updateViewHolder(pendingDownload, holder) + notifyItemChanged(position); + return@setOnClickListener + } + + pendingDownload.outputFile?.let { + fun showFileNotFound() { + Toast.makeText(holder.view.context, SharedContext.translation["download_manager_activity.file_not_found_toast"], Toast.LENGTH_SHORT).show() + } + + val uri = Uri.parse(it) + val fileType = runCatching { + if (uri.scheme == "content") { + activity.contentResolver.openInputStream(uri)?.use { input -> + FileType.fromInputStream(input) + } ?: run { + showFileNotFound() + return@setOnClickListener + } + } else { + val file = File(it) + if (!file.exists()) { + showFileNotFound() + return@setOnClickListener + } + FileType.fromFile(file) + } + }.onFailure { exception -> + Logger.error("Failed to open file", exception) + }.getOrDefault(FileType.UNKNOWN) + if (fileType == FileType.UNKNOWN) { + showFileNotFound() + return@setOnClickListener + } + + val intent = Intent(Intent.ACTION_VIEW) + intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION + intent.setDataAndType(uri, fileType.mimeType) + holder.view.context.startActivity(intent) + } + } + + updateViewHolder(pendingDownload, holder) + } +}+ \ No newline at end of file diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/DownloadManagerActivity.kt @@ -0,0 +1,214 @@ +package me.rhunk.snapenhance.ui.download + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Bundle +import android.os.PowerManager +import android.provider.Settings +import android.view.View +import android.widget.Button +import android.widget.ImageButton +import android.widget.TextView +import androidx.recyclerview.widget.ItemTouchHelper +import androidx.recyclerview.widget.RecyclerView +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.core.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.bridge.wrapper.TranslationWrapper +import me.rhunk.snapenhance.download.data.PendingDownload + +class DownloadManagerActivity : Activity() { + lateinit var translation: TranslationWrapper + + private val backCallbacks = mutableListOf<() -> Unit>() + private val fetchedDownloadTasks = mutableListOf<PendingDownload>() + private var listFilter = MediaFilter.NONE + + private val preferences by lazy { + getSharedPreferences("settings", Context.MODE_PRIVATE) + } + + private fun updateNoDownloadText() { + findViewById<TextView>(R.id.no_download_title).let { + it.text = translation["no_downloads"] + it.visibility = if (fetchedDownloadTasks.isEmpty()) View.VISIBLE else View.GONE + } + } + + @SuppressLint("NotifyDataSetChanged") + private fun updateListContent() { + fetchedDownloadTasks.clear() + fetchedDownloadTasks.addAll(SharedContext.downloadTaskManager.queryAllTasks(filter = listFilter).values) + + with(findViewById<RecyclerView>(R.id.download_list)) { + adapter?.notifyDataSetChanged() + scrollToPosition(0) + } + updateNoDownloadText() + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onBackPressed() { + backCallbacks.lastOrNull()?.let { + it() + backCallbacks.removeLast() + } ?: super.onBackPressed() + } + + fun registerBackCallback(callback: () -> Unit) { + backCallbacks.add(callback) + } + + @SuppressLint("BatteryLife", "NotifyDataSetChanged", "SetTextI18n") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + SharedContext.ensureInitialized(this) + translation = SharedContext.translation.getCategory("download_manager_activity") + + setContentView(R.layout.download_manager_activity) + + findViewById<TextView>(R.id.title).text = resources.getString(R.string.app_name) + " " + BuildConfig.VERSION_NAME + + findViewById<ImageButton>(R.id.debug_settings_button).setOnClickListener { + DebugSettingsLayoutInflater(this).inflate(findViewById(android.R.id.content)) + } + + with(findViewById<RecyclerView>(R.id.download_list)) { + layoutManager = androidx.recyclerview.widget.LinearLayoutManager(this@DownloadManagerActivity) + + adapter = DownloadListAdapter(this@DownloadManagerActivity, fetchedDownloadTasks).apply { + registerAdapterDataObserver(object : RecyclerView.AdapterDataObserver() { + override fun onItemRangeRemoved(positionStart: Int, itemCount: Int) { + updateNoDownloadText() + } + }) + } + + ItemTouchHelper(object : ItemTouchHelper.SimpleCallback(0, ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { + override fun getMovementFlags( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder + ): Int { + val download = fetchedDownloadTasks[viewHolder.absoluteAdapterPosition] + return if (download.isJobActive()) { + 0 + } else { + super.getMovementFlags(recyclerView, viewHolder) + } + } + + override fun onMove( + recyclerView: RecyclerView, + viewHolder: RecyclerView.ViewHolder, + target: RecyclerView.ViewHolder + ): Boolean { + return false + } + + @SuppressLint("NotifyDataSetChanged") + override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) { + fetchedDownloadTasks.removeAt(viewHolder.absoluteAdapterPosition).let { + SharedContext.downloadTaskManager.removeTask(it) + } + adapter?.notifyItemRemoved(viewHolder.absoluteAdapterPosition) + } + }).attachToRecyclerView(this) + + var isLoading = false + + addOnScrollListener(object : RecyclerView.OnScrollListener() { + override fun onScrollStateChanged(recyclerView: RecyclerView, newState: Int) { + val layoutManager = recyclerView.layoutManager as androidx.recyclerview.widget.LinearLayoutManager + val lastVisibleItemPosition = layoutManager.findLastVisibleItemPosition() + + if (lastVisibleItemPosition == RecyclerView.NO_POSITION) { + return + } + + if (lastVisibleItemPosition == fetchedDownloadTasks.size - 1 && !isLoading) { + isLoading = true + + SharedContext.downloadTaskManager.queryTasks(fetchedDownloadTasks.last().downloadId, filter = listFilter).forEach { + fetchedDownloadTasks.add(it.value) + adapter?.notifyItemInserted(fetchedDownloadTasks.size - 1) + } + + isLoading = false + } + } + }) + + arrayOf( + Pair(R.id.all_category, MediaFilter.NONE), + Pair(R.id.pending_category, MediaFilter.PENDING), + Pair(R.id.snap_category, MediaFilter.CHAT_MEDIA), + Pair(R.id.story_category, MediaFilter.STORY), + Pair(R.id.spotlight_category, MediaFilter.SPOTLIGHT) + ).let { categoryPairs -> + categoryPairs.forEach { pair -> + this@DownloadManagerActivity.findViewById<TextView>(pair.first).apply { + text = translation["category.${resources.getResourceEntryName(pair.first)}"] + }.setOnClickListener { view -> + listFilter = pair.second + updateListContent() + categoryPairs.map { this@DownloadManagerActivity.findViewById<TextView>(it.first) }.forEach { + it.setTextColor(getColor(R.color.primaryText)) + } + (view as TextView).setTextColor(getColor(R.color.focusedCategoryColor)) + } + } + } + + this@DownloadManagerActivity.findViewById<Button>(R.id.remove_all_button).also { + it.text = translation["remove_all"] + }.setOnClickListener { + with(AlertDialog.Builder(this@DownloadManagerActivity)) { + setTitle(translation["remove_all_title"]) + setMessage(translation["remove_all_text"]) + setPositiveButton(SharedContext.translation["button.positive"]) { _, _ -> + SharedContext.downloadTaskManager.removeAllTasks() + fetchedDownloadTasks.removeIf { + if (it.isJobActive()) it.cancel() + true + } + adapter?.notifyDataSetChanged() + updateNoDownloadText() + } + setNegativeButton(SharedContext.translation["button.negative"]) { dialog, _ -> + dialog.dismiss() + } + show() + } + } + + } + + updateListContent() + + if (!preferences.getBoolean("ask_battery_optimisations", true) || + !(getSystemService(Context.POWER_SERVICE) as PowerManager).isIgnoringBatteryOptimizations(packageName)) return + + with(Intent()) { + action = Settings.ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS + data = Uri.parse("package:$packageName") + startActivityForResult(this, 1) + } + } + + override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { + if (requestCode == 1) { + preferences.edit().putBoolean("ask_battery_optimisations", false).apply() + } + } + + @SuppressLint("NotifyDataSetChanged") + override fun onResume() { + super.onResume() + updateListContent() + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/download/MediaFilter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/download/MediaFilter.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/map/MapActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/map/MapActivity.kt @@ -0,0 +1,98 @@ +package me.rhunk.snapenhance.ui.map + +import android.annotation.SuppressLint +import android.app.Activity +import android.app.AlertDialog +import android.content.Context +import android.os.Bundle +import android.view.MotionEvent +import android.widget.Button +import android.widget.EditText +import me.rhunk.snapenhance.core.R +import org.osmdroid.config.Configuration +import org.osmdroid.tileprovider.tilesource.TileSourceFactory +import org.osmdroid.util.GeoPoint +import org.osmdroid.views.MapView +import org.osmdroid.views.Projection +import org.osmdroid.views.overlay.Marker +import org.osmdroid.views.overlay.Overlay + + +class MapActivity : Activity() { + + private lateinit var mapView: MapView + + @SuppressLint("MissingInflatedId", "ResourceType") + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + + val contextBundle = intent.extras?.getBundle("location") ?: return + val locationLatitude = contextBundle.getDouble("latitude") + val locationLongitude = contextBundle.getDouble("longitude") + + Configuration.getInstance().load(applicationContext, getSharedPreferences("osmdroid", Context.MODE_PRIVATE)) + + setContentView(R.layout.map) + + mapView = findViewById(R.id.mapView) + mapView.setMultiTouchControls(true); + mapView.setTileSource(TileSourceFactory.MAPNIK) + + val startPoint = GeoPoint(locationLatitude, locationLongitude) + mapView.controller.setZoom(10.0) + mapView.controller.setCenter(startPoint) + + val marker = Marker(mapView) + marker.isDraggable = true + marker.position = startPoint + marker.setAnchor(Marker.ANCHOR_CENTER, Marker.ANCHOR_BOTTOM) + + mapView.overlays.add(object: Overlay() { + override fun onSingleTapConfirmed(e: MotionEvent?, mapView: MapView?): Boolean { + val proj: Projection = mapView!!.projection + val loc = proj.fromPixels(e!!.x.toInt(), e.y.toInt()) as GeoPoint + marker.position = loc + mapView.invalidate() + return true + } + }) + + mapView.overlays.add(marker) + + val applyButton = findViewById<Button>(R.id.apply_location_button) + applyButton.setOnClickListener { + val bundle = Bundle() + bundle.putFloat("latitude", marker.position.latitude.toFloat()) + bundle.putFloat("longitude", marker.position.longitude.toFloat()) + setResult(RESULT_OK, intent.putExtra("location", bundle)) + finish() + } + + val setPreciseLocationButton = findViewById<Button>(R.id.set_precise_location_button) + + setPreciseLocationButton.setOnClickListener { + val locationDialog = layoutInflater.inflate(R.layout.precise_location_dialog, null) + val dialogLatitude = locationDialog.findViewById<EditText>(R.id.dialog_latitude).also { it.setText(marker.position.latitude.toString()) } + val dialogLongitude = locationDialog.findViewById<EditText>(R.id.dialog_longitude).also { it.setText(marker.position.longitude.toString()) } + + AlertDialog.Builder(this) + .setView(locationDialog) + .setTitle("Set a precise location") + .setPositiveButton("Set") { _, _ -> + val latitude = dialogLatitude.text.toString().toDoubleOrNull() + val longitude = dialogLongitude.text.toString().toDoubleOrNull() + if (latitude != null && longitude != null) { + val preciseLocation = GeoPoint(latitude, longitude) + mapView.controller.setCenter(preciseLocation) + marker.position = preciseLocation + mapView.invalidate() + } + }.setNegativeButton("Cancel") { _, _ -> }.show() + } + } + + override fun onDestroy() { + super.onDestroy() + mapView.onDetach() + } +} diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/AbstractMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/AbstractMenu.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/ChatActionMenu.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/FriendFeedInfoMenu.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/MenuViewInjector.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/OperaContextActionMenu.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsGearInjector.kt @@ -0,0 +1,86 @@ +package me.rhunk.snapenhance.ui.menu.impl + +import android.annotation.SuppressLint +import android.content.Intent +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import android.widget.ImageView +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.Constants +import me.rhunk.snapenhance.ui.config.ConfigActivity +import me.rhunk.snapenhance.ui.menu.AbstractMenu +import java.io.File + + +@SuppressLint("DiscouragedApi") +class SettingsGearInjector : AbstractMenu() { + private val headerButtonOpaqueIconTint by lazy { + context.resources.getIdentifier("headerButtonOpaqueIconTint", "attr", Constants.SNAPCHAT_PACKAGE_NAME).let { + context.androidContext.theme.obtainStyledAttributes(intArrayOf(it)).getColorStateList(0) + } + } + + private val settingsSvg by lazy { + context.resources.getIdentifier("svg_settings_32x32", "drawable", Constants.SNAPCHAT_PACKAGE_NAME).let { + context.resources.getDrawable(it, context.androidContext.theme) + } + } + + private val ngsHovaHeaderSearchIconBackgroundMarginLeft by lazy { + context.resources.getIdentifier("ngs_hova_header_search_icon_background_margin_left", "dimen", Constants.SNAPCHAT_PACKAGE_NAME).let { + context.resources.getDimensionPixelSize(it) + } + } + + @SuppressLint("SetTextI18n", "ClickableViewAccessibility") + fun inject(parent: ViewGroup, child: View) { + val firstView = (child as ViewGroup).getChildAt(0) + + child.clipChildren = false + child.addView(FrameLayout(parent.context).apply { + layoutParams = FrameLayout.LayoutParams(firstView.layoutParams.width, firstView.layoutParams.height).apply { + y = 0f + x = -(ngsHovaHeaderSearchIconBackgroundMarginLeft + firstView.layoutParams.width).toFloat() + } + + isClickable = true + + setOnClickListener { + val intent = Intent().apply { + setClassName(BuildConfig.LIBRARY_PACKAGE_NAME, ConfigActivity::class.java.name) + } + intent.putExtra("lspatched", File(context.cacheDir, "lspatch/origin").exists()) + context.startActivity(intent) + } + + parent.setOnTouchListener { _, event -> + if (child.visibility == View.INVISIBLE || child.alpha == 0F) return@setOnTouchListener false + + val viewLocation = IntArray(2) + getLocationOnScreen(viewLocation) + + val x = event.rawX - viewLocation[0] + val y = event.rawY - viewLocation[1] + + if (x > 0 && x < width && y > 0 && y < height) { + performClick() + } + + false + } + backgroundTintList = firstView.backgroundTintList + background = firstView.background + + addView(ImageView(context).apply { + layoutParams = FrameLayout.LayoutParams(ViewGroup.LayoutParams.WRAP_CONTENT, ViewGroup.LayoutParams.WRAP_CONTENT, 17).apply { + gravity = android.view.Gravity.CENTER + } + setImageDrawable(settingsSvg) + headerButtonOpaqueIconTint?.let { + imageTintList = it + } + }) + }) + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/menu/impl/SettingsMenu.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt b/core/src/main/kotlin/me/rhunk/snapenhance/ui/spoof/DeviceSpooferActivity.kt @@ -0,0 +1,111 @@ +package me.rhunk.snapenhance.ui.spoof + +import android.app.Activity +import android.content.res.ColorStateList +import android.os.Bundle +import android.text.InputType +import android.view.View +import android.view.ViewGroup +import android.widget.ImageButton +import android.widget.Switch +import android.widget.TextView +import me.rhunk.snapenhance.core.R +import me.rhunk.snapenhance.SharedContext +import me.rhunk.snapenhance.config.ConfigCategory +import me.rhunk.snapenhance.config.impl.ConfigIntegerValue +import me.rhunk.snapenhance.config.impl.ConfigStateValue +import me.rhunk.snapenhance.config.impl.ConfigStringValue +import me.rhunk.snapenhance.ui.ItemHelper + +class DeviceSpooferActivity: Activity() { + private val itemHelper = ItemHelper(this) + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + SharedContext.ensureInitialized(this) + setContentView(R.layout.device_spoofer_activity) + + findViewById<TextView>(R.id.title).text = "Device Spoofer" + findViewById<ImageButton>(R.id.back_button).setOnClickListener { finish() } + val propertyListLayout = findViewById<ViewGroup>(R.id.spoof_property_list) + + SharedContext.config.entries().filter { it.key.category == ConfigCategory.DEVICE_SPOOFER }.forEach { (property, value) -> + val configItem = layoutInflater.inflate(R.layout.config_activity_item, propertyListLayout, false) + + val propertyName = SharedContext.translation["property.${property.translationKey}.name"] + + fun addSeparator() { + //add separator + propertyListLayout.addView(View(this).apply { + layoutParams = ViewGroup.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, 1) + setBackgroundColor(getColor(R.color.tertiaryBackground)) + }) + } + + configItem.findViewById<TextView>(R.id.name).text = propertyName + configItem.findViewById<TextView>(R.id.description).also { + it.text = SharedContext.translation["property.${property.translationKey}.description"] + it.visibility = if (it.text.isEmpty()) View.GONE else View.VISIBLE + } + + fun addValueView(view: View) { + configItem.findViewById<ViewGroup>(R.id.value).addView(view) + } + + when (value) { + is ConfigStateValue -> { + val switch = Switch(this) + switch.isChecked = value.value() + switch.trackTintList = ColorStateList( + arrayOf( + intArrayOf(android.R.attr.state_checked), + intArrayOf(-android.R.attr.state_checked) + ), + intArrayOf( + switch.highlightColor, + getColor(R.color.tertiaryBackground) + ) + ) + switch.setOnCheckedChangeListener { _, isChecked -> + value.writeFrom(isChecked.toString()) + } + configItem.setOnClickListener { switch.toggle() } + addValueView(switch) + } + is ConfigStringValue, is ConfigIntegerValue -> { + val textView = itemHelper.createTranslatedTextView(property, shouldTranslatePropertyValue = false).also { + it.text = value.value().toString() + } + configItem.setOnClickListener { + if (value is ConfigIntegerValue) { + itemHelper.askForValue(property, InputType.TYPE_CLASS_NUMBER) { + try { + value.writeFrom(it) + textView.text = value.value().toString() + } catch (e: NumberFormatException) { + itemHelper.longToast(SharedContext.translation["config_activity.invalid_number_toast"], this) + } + } + return@setOnClickListener + } + itemHelper.askForValue(property, InputType.TYPE_CLASS_TEXT) { + value.writeFrom(it) + textView.text = value.value().toString() + } + } + addValueView(textView) + } + } + + propertyListLayout.addView(configItem) + addSeparator() + } + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + override fun onBackPressed() { + super.onBackPressed() + finish() + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ActivityResultCallback.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/AndroidCompatExtensions.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/CallbackBuilder.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/ReflectionHelper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/SQLiteDatabaseHelper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/XposedHelperMacros.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/DownloadServer.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/download/RemoteMediaResolver.kt diff --git a/core/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/export/MessageExporter.kt @@ -0,0 +1,337 @@ +package me.rhunk.snapenhance.util.export + +import android.content.pm.PackageManager +import android.os.Environment +import android.util.Base64InputStream +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import de.robv.android.xposed.XposedHelpers +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.withContext +import me.rhunk.snapenhance.core.BuildConfig +import me.rhunk.snapenhance.Logger +import me.rhunk.snapenhance.ModContext +import me.rhunk.snapenhance.data.ContentType +import me.rhunk.snapenhance.data.FileType +import me.rhunk.snapenhance.data.MediaReferenceType +import me.rhunk.snapenhance.data.wrapper.impl.Message +import me.rhunk.snapenhance.data.wrapper.impl.SnapUUID +import me.rhunk.snapenhance.database.objects.FriendFeedInfo +import me.rhunk.snapenhance.database.objects.FriendInfo +import me.rhunk.snapenhance.util.getApplicationInfoCompat +import me.rhunk.snapenhance.util.protobuf.ProtoReader +import me.rhunk.snapenhance.util.snap.EncryptionHelper +import me.rhunk.snapenhance.util.snap.MediaDownloaderHelper +import java.io.File +import java.io.FileOutputStream +import java.io.InputStream +import java.io.OutputStream +import java.text.SimpleDateFormat +import java.util.Base64 +import java.util.Collections +import java.util.Date +import java.util.Locale +import java.util.zip.Deflater +import java.util.zip.DeflaterInputStream +import java.util.zip.ZipFile +import kotlin.io.encoding.ExperimentalEncodingApi + + +enum class ExportFormat( + val extension: String, +){ + JSON("json"), + TEXT("txt"), + HTML("html"); +} + +class MessageExporter( + private val context: ModContext, + private val outputFile: File, + private val friendFeedInfo: FriendFeedInfo, + private val mediaToDownload: List<ContentType>? = null, + private val printLog: (String) -> Unit = {}, +) { + private lateinit var conversationParticipants: Map<String, FriendInfo> + private lateinit var messages: List<Message> + + fun readMessages(messages: List<Message>) { + conversationParticipants = + context.database.getConversationParticipants(friendFeedInfo.key!!) + ?.mapNotNull { + context.database.getFriendInfo(it) + }?.associateBy { it.userId!! } ?: emptyMap() + + if (conversationParticipants.isEmpty()) + throw Throwable("Failed to get conversation participants for ${friendFeedInfo.key}") + + this.messages = messages.sortedBy { it.orderKey } + } + + private fun serializeMessageContent(message: Message): String? { + return if (message.messageContent.contentType == ContentType.CHAT) { + ProtoReader(message.messageContent.content).getString(2, 1) ?: "Failed to parse message" + } else null + } + + private fun exportText(output: OutputStream) { + val writer = output.bufferedWriter() + writer.write("Conversation key: ${friendFeedInfo.key}\n") + writer.write("Conversation Name: ${friendFeedInfo.feedDisplayName}\n") + writer.write("Participants:\n") + conversationParticipants.forEach { (userId, friendInfo) -> + writer.write(" $userId: ${friendInfo.displayName}\n") + } + + writer.write("\nMessages:\n") + messages.forEach { message -> + val sender = conversationParticipants[message.senderId.toString()] + val senderUsername = sender?.usernameForSorting ?: message.senderId.toString() + val senderDisplayName = sender?.displayName ?: message.senderId.toString() + val messageContent = serializeMessageContent(message) ?: message.messageContent.contentType.name + val date = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.ENGLISH).format(Date(message.messageMetadata.createdAt)) + writer.write("[$date] - $senderDisplayName (${senderUsername}): $messageContent\n") + } + writer.flush() + } + + @OptIn(ExperimentalEncodingApi::class) + suspend fun exportHtml(output: OutputStream) { + val downloadMediaCacheFolder = File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS), "SnapEnhance/cache").also { it.mkdirs() } + val mediaFiles = Collections.synchronizedMap(mutableMapOf<String, Pair<FileType, File>>()) + + printLog("found ${messages.size} messages") + + withContext(Dispatchers.IO) { + messages.filter { + mediaToDownload?.contains(it.messageContent.contentType) ?: false + }.map { message -> + async { + val remoteMediaReferences by lazy { + val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject + serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + } + + remoteMediaReferences.firstOrNull().takeIf { it != null }?.let { media -> + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + + runCatching { + val downloadedMedia = MediaDownloaderHelper.downloadMediaFromReference(protoMediaReference) { + EncryptionHelper.decryptInputStream(it, message.messageContent.contentType, ProtoReader(message.messageContent.content), isArroyo = false) + } + + printLog("downloaded media ${message.orderKey}") + + downloadedMedia.forEach { (type, mediaData) -> + val fileType = FileType.fromByteArray(mediaData) + val fileName = "${type}_${kotlin.io.encoding.Base64.UrlSafe.encode(protoMediaReference).replace("=", "")}" + + val mediaFile = File(downloadMediaCacheFolder, "$fileName.${fileType.fileExtension}") + + FileOutputStream(mediaFile).use { fos -> + mediaData.inputStream().copyTo(fos) + } + + mediaFiles[fileName] = fileType to mediaFile + } + }.onFailure { + printLog("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}") + Logger.error("failed to download media for ${message.messageDescriptor.conversationId}_${message.orderKey}", it) + } + } + } + }.awaitAll() + } + + printLog("writing downloaded medias...") + + //write the head of the html file + output.write(""" + <!DOCTYPE html> + <html> + <head> + <meta charset="UTF-8"> + <meta http-equiv="X-UA-Compatible" content="IE=edge"> + <meta name="viewport" content="width=device-width, initial-scale=1.0"> + <title></title> + </head> + """.trimIndent().toByteArray()) + + output.write("<!-- This file was generated by SnapEnhance ${BuildConfig.VERSION_NAME} -->\n".toByteArray()) + + mediaFiles.forEach { (key, filePair) -> + printLog("writing $key...") + output.write("<div class=\"media-$key\"><!-- ".toByteArray()) + + val deflateInputStream = DeflaterInputStream(filePair.second.inputStream(), Deflater(Deflater.BEST_COMPRESSION, true)) + val base64InputStream = XposedHelpers.newInstance( + Base64InputStream::class.java, + deflateInputStream, + android.util.Base64.DEFAULT or android.util.Base64.NO_WRAP, + true + ) as InputStream + base64InputStream.copyTo(output) + deflateInputStream.close() + + output.write(" --></div>\n".toByteArray()) + output.flush() + } + printLog("writing json conversation data...") + + //write the json file + output.write("<script type=\"application/json\" class=\"exported_content\">".toByteArray()) + exportJson(output) + output.write("</script>\n".toByteArray()) + + printLog("writing template...") + + runCatching { + ZipFile( + context.androidContext.packageManager.getApplicationInfoCompat(BuildConfig.LIBRARY_PACKAGE_NAME, PackageManager.GET_META_DATA).publicSourceDir + ).use { apkFile -> + //export rawinflate.js + apkFile.getEntry("assets/web/rawinflate.js").let { entry -> + output.write("<script>".toByteArray()) + apkFile.getInputStream(entry).copyTo(output) + output.write("</script>\n".toByteArray()) + } + + //export avenir next font + apkFile.getEntry("res/font/avenir_next_medium.ttf").let { entry -> + val encodedFontData = kotlin.io.encoding.Base64.Default.encode(apkFile.getInputStream(entry).readBytes()) + output.write(""" + <style> + @font-face { + font-family: 'Avenir Next'; + src: url('data:font/truetype;charset=utf-8;base64, $encodedFontData'); + font-weight: normal; + font-style: normal; + } + </style> + """.trimIndent().toByteArray()) + } + + apkFile.getEntry("assets/web/export_template.html").let { entry -> + apkFile.getInputStream(entry).copyTo(output) + } + + apkFile.close() + } + }.onFailure { + printLog("failed to read template from apk") + Logger.error("failed to read template from apk", it) + } + + output.write("</html>".toByteArray()) + output.close() + printLog("done") + } + + private fun exportJson(output: OutputStream) { + val rootObject = JsonObject().apply { + addProperty("conversationId", friendFeedInfo.key) + addProperty("conversationName", friendFeedInfo.feedDisplayName) + + var index = 0 + val participants = mutableMapOf<String, Int>() + + add("participants", JsonObject().apply { + conversationParticipants.forEach { (userId, friendInfo) -> + add(userId, JsonObject().apply { + addProperty("id", index) + addProperty("displayName", friendInfo.displayName) + addProperty("username", friendInfo.usernameForSorting) + addProperty("bitmojiSelfieId", friendInfo.bitmojiSelfieId) + }) + participants[userId] = index++ + } + }) + add("messages", JsonArray().apply { + messages.forEach { message -> + add(JsonObject().apply { + addProperty("orderKey", message.orderKey) + addProperty("senderId", participants.getOrDefault(message.senderId.toString(), -1)) + addProperty("type", message.messageContent.contentType.toString()) + + fun addUUIDList(name: String, list: List<SnapUUID>) { + add(name, JsonArray().apply { + list.map { participants.getOrDefault(it.toString(), -1) }.forEach { add(it) } + }) + } + + addUUIDList("savedBy", message.messageMetadata.savedBy) + addUUIDList("seenBy", message.messageMetadata.seenBy) + addUUIDList("openedBy", message.messageMetadata.openedBy) + + add("reactions", JsonObject().apply { + message.messageMetadata.reactions.forEach { reaction -> + addProperty( + participants.getOrDefault(reaction.userId.toString(), -1L).toString(), + reaction.reactionId + ) + } + }) + + addProperty("createdTimestamp", message.messageMetadata.createdAt) + addProperty("readTimestamp", message.messageMetadata.readAt) + addProperty("serializedContent", serializeMessageContent(message)) + addProperty("rawContent", Base64.getUrlEncoder().encodeToString(message.messageContent.content)) + + val messageContentType = message.messageContent.contentType + + EncryptionHelper.getEncryptionKeys(messageContentType, ProtoReader(message.messageContent.content), isArroyo = false)?.let { encryptionKeyPair -> + add("encryption", JsonObject().apply encryption@{ + addProperty("key", Base64.getEncoder().encodeToString(encryptionKeyPair.first)) + addProperty("iv", Base64.getEncoder().encodeToString(encryptionKeyPair.second)) + }) + } + + val remoteMediaReferences by lazy { + val serializedMessageContent = context.gson.toJsonTree(message.messageContent.instanceNonNull()).asJsonObject + serializedMessageContent["mRemoteMediaReferences"] + .asJsonArray.map { it.asJsonObject["mMediaReferences"].asJsonArray } + .flatten() + } + + add("mediaReferences", JsonArray().apply mediaReferences@ { + if (messageContentType != ContentType.EXTERNAL_MEDIA && + messageContentType != ContentType.STICKER && + messageContentType != ContentType.SNAP && + messageContentType != ContentType.NOTE) + return@mediaReferences + + remoteMediaReferences.forEach { media -> + val protoMediaReference = media.asJsonObject["mContentObject"].asJsonArray.map { it.asByte }.toByteArray() + val mediaType = MediaReferenceType.valueOf(media.asJsonObject["mMediaType"].asString) + add(JsonObject().apply { + addProperty("mediaType", mediaType.toString()) + addProperty("content", Base64.getUrlEncoder().encodeToString(protoMediaReference)) + }) + } + }) + + }) + } + }) + } + + output.write(context.gson.toJson(rootObject).toByteArray()) + output.flush() + } + + suspend fun exportTo(exportFormat: ExportFormat) { + val output = FileOutputStream(outputFile) + + when (exportFormat) { + ExportFormat.HTML -> exportHtml(output) + ExportFormat.JSON -> exportJson(output) + ExportFormat.TEXT -> exportText(output) + } + + output.close() + } +}+ \ No newline at end of file diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoEditor.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoReader.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/protobuf/ProtoWriter.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/BitmojiSelfie.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/EncryptionHelper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/MediaDownloaderHelper.kt diff --git a/app/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt b/core/src/main/kotlin/me/rhunk/snapenhance/util/snap/PreviewUtils.kt diff --git a/app/src/main/res/drawable/action_button_cancel.xml b/core/src/main/res/drawable/action_button_cancel.xml diff --git a/app/src/main/res/drawable/action_button_success.xml b/core/src/main/res/drawable/action_button_success.xml diff --git a/app/src/main/res/drawable/back_arrow.xml b/core/src/main/res/drawable/back_arrow.xml diff --git a/app/src/main/res/drawable/bitmoji_blank.xml b/core/src/main/res/drawable/bitmoji_blank.xml diff --git a/app/src/main/res/drawable/debug_settings_icon.xml b/core/src/main/res/drawable/debug_settings_icon.xml diff --git a/app/src/main/res/drawable/download_manager_item_background.xml b/core/src/main/res/drawable/download_manager_item_background.xml diff --git a/app/src/main/res/drawable/settings_icon.xml b/core/src/main/res/drawable/settings_icon.xml diff --git a/app/src/main/res/font/avenir_next_bold.ttf b/core/src/main/res/font/avenir_next_bold.ttf Binary files differ. diff --git a/app/src/main/res/font/avenir_next_medium.ttf b/core/src/main/res/font/avenir_next_medium.ttf Binary files differ. diff --git a/app/src/main/res/layout/activity_default_header.xml b/core/src/main/res/layout/activity_default_header.xml diff --git a/app/src/main/res/layout/config_activity.xml b/core/src/main/res/layout/config_activity.xml diff --git a/app/src/main/res/layout/config_activity_debug_item.xml b/core/src/main/res/layout/config_activity_debug_item.xml diff --git a/app/src/main/res/layout/config_activity_item.xml b/core/src/main/res/layout/config_activity_item.xml diff --git a/app/src/main/res/layout/debug_setting_item.xml b/core/src/main/res/layout/debug_setting_item.xml diff --git a/app/src/main/res/layout/debug_settings_page.xml b/core/src/main/res/layout/debug_settings_page.xml diff --git a/app/src/main/res/layout/device_spoofer_activity.xml b/core/src/main/res/layout/device_spoofer_activity.xml diff --git a/app/src/main/res/layout/download_manager_activity.xml b/core/src/main/res/layout/download_manager_activity.xml diff --git a/app/src/main/res/layout/download_manager_item.xml b/core/src/main/res/layout/download_manager_item.xml diff --git a/app/src/main/res/layout/map.xml b/core/src/main/res/layout/map.xml diff --git a/app/src/main/res/layout/precise_location_dialog.xml b/core/src/main/res/layout/precise_location_dialog.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher_icon.xml b/core/src/main/res/mipmap-anydpi-v26/launcher_icon.xml diff --git a/app/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml b/core/src/main/res/mipmap-anydpi-v26/launcher_icon_round.xml diff --git a/app/src/main/res/mipmap-hdpi/launcher_icon_foreground.png b/core/src/main/res/mipmap-hdpi/launcher_icon_foreground.png Binary files differ. diff --git a/app/src/main/res/mipmap-hdpi/launcher_icon_round.png b/core/src/main/res/mipmap-hdpi/launcher_icon_round.png Binary files differ. diff --git a/app/src/main/res/mipmap-mdpi/launcher_icon_foreground.png b/core/src/main/res/mipmap-mdpi/launcher_icon_foreground.png Binary files differ. diff --git a/app/src/main/res/mipmap-mdpi/launcher_icon_round.png b/core/src/main/res/mipmap-mdpi/launcher_icon_round.png Binary files differ. diff --git a/app/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png b/core/src/main/res/mipmap-xhdpi/launcher_icon_foreground.png Binary files differ. diff --git a/app/src/main/res/mipmap-xhdpi/launcher_icon_round.png b/core/src/main/res/mipmap-xhdpi/launcher_icon_round.png Binary files differ. diff --git a/app/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png b/core/src/main/res/mipmap-xxhdpi/launcher_icon_foreground.png Binary files differ. diff --git a/app/src/main/res/mipmap-xxhdpi/launcher_icon_round.png b/core/src/main/res/mipmap-xxhdpi/launcher_icon_round.png Binary files differ. diff --git a/app/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png b/core/src/main/res/mipmap-xxxhdpi/launcher_icon_foreground.png Binary files differ. diff --git a/app/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png b/core/src/main/res/mipmap-xxxhdpi/launcher_icon_round.png Binary files differ. diff --git a/app/src/main/res/values/arrays.xml b/core/src/main/res/values/arrays.xml diff --git a/app/src/main/res/values/colors.xml b/core/src/main/res/values/colors.xml diff --git a/app/src/main/res/values/dimens.xml b/core/src/main/res/values/dimens.xml diff --git a/app/src/main/res/values/launcher_icon_background.xml b/core/src/main/res/values/launcher_icon_background.xml diff --git a/app/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml diff --git a/app/src/main/res/values/themes.xml b/core/src/main/res/values/themes.xml diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ osmdroid-android = "6.1.16" okhttp = "5.0.0-alpha.11" dexlib2 = "2.5.2" androidx-documentfile = "1.1.0-alpha01" +activity-ktx = "1.7.2" [libraries] @@ -24,6 +25,7 @@ osmdroid-android = { group = "org.osmdroid", name = "osmdroid-android", version. okhttp = { group = "com.squareup.okhttp3", name = "okhttp", version.ref = "okhttp" } dexlib2 = { group = "org.smali", name = "dexlib2", version.ref = "dexlib2" } androidx-documentfile = { group = "androidx.documentfile", name = "documentfile", version.ref = "androidx-documentfile" } +androidx-activity-ktx = { group = "androidx.activity", name = "activity-ktx", version.ref = "activity-ktx" } [plugins] diff --git a/settings.gradle.kts b/settings.gradle.kts @@ -15,6 +15,8 @@ dependencyResolutionManagement { } } + rootProject.name = "SnapEnhance" +include(":core") include(":app") include(":mapper") \ No newline at end of file